Repository: cargo-lambda/cargo-lambda (language: python)

Issue:
support deploying internal extensions
i could just be missing something, but it seems like there's no way to deploy internal extensions through cargo lambda. using the `--extension` flag during building/deploying forces your binary to be deployed at `/opt/extensions/mybinary`. The issue is; internal extensions need to be deployed at `/opt/mybinary` or else they'll be treated like an external extension.


Discussion / hints:
is there any documentation about "internal" vs "external"? I'm not familiar with the difference.
> is there any documentation about "internal" vs "external"? I'm not familiar with the difference.

[This page](https://docs.aws.amazon.com/lambda/latest/dg/lambda-extensions.html) has an explanation of the difference, although they're both fairly similar. [This page](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html) shows an example of an internal extension. The only change needed to cargo-lambda would (hopefully) be some kind of `internal` flag that says the extension is an internal one and changes its path

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index c5725e8d..ce7804bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -806,6 +806,7 @@ dependencies = [
  "tokio",
  "tracing",
  "tracing-subscriber",
+ "zip",
 ]
 
 [[package]]
diff --git a/crates/cargo-lambda-build/src/lib.rs b/crates/cargo-lambda-build/src/lib.rs
index f21f3366..20382fdb 100644
--- a/crates/cargo-lambda-build/src/lib.rs
+++ b/crates/cargo-lambda-build/src/lib.rs
@@ -64,6 +64,10 @@ pub struct Build {
     #[arg(long)]
     extension: bool,
 
+    /// Whether an extension is internal or external
+    #[arg(long, requires = "extension")]
+    internal: bool,
+
     /// Put a bootstrap file in the root of the lambda directory.
     /// Use the name of the compiled binary to choose which file to move.
     #[arg(long)]
@@ -251,7 +255,7 @@ impl Build {
                         copy_and_replace(binary, bootstrap_dir.join(bin_name)).into_diagnostic()?;
                     }
                     OutputFormat::Zip => {
-                        let parent = if self.extension {
+                        let parent = if self.extension && !self.internal {
                             Some("extensions")
                         } else {
                             None
@@ -282,13 +286,16 @@ pub fn find_binary_archive<M, P>(
     manifest_path: M,
     base_dir: &Option<P>,
     is_extension: bool,
+    is_internal: bool,
 ) -> Result<BinaryArchive>
 where
     M: AsRef<Path> + fmt::Debug,
     P: AsRef<Path>,
 {
     let target_dir = target_dir(manifest_path).unwrap_or_else(|_| PathBuf::from("target"));
-    let (dir_name, binary_name, parent) = if is_extension {
+    let (dir_name, binary_name, parent) = if is_extension && is_internal {
+        ("extensions", name, None)
+    } else if is_extension && !is_internal {
         ("extensions", name, Some("extensions"))
     } else {
         (name, "bootstrap", None)
@@ -302,7 +309,9 @@ where
 
     let binary_path = bootstrap_dir.join(binary_name);
     if !binary_path.exists() {
-        let build_cmd = if is_extension {
+        let build_cmd = if is_extension && is_internal {
+            "build --extension --internal"
+        } else if is_extension && !is_internal {
             "build --extension"
         } else {
             "build"
diff --git a/crates/cargo-lambda-cli/Cargo.toml b/crates/cargo-lambda-cli/Cargo.toml
index c10a18cc..1efd156f 100644
--- a/crates/cargo-lambda-cli/Cargo.toml
+++ b/crates/cargo-lambda-cli/Cargo.toml
@@ -36,3 +36,4 @@ cargo-test-macro = { git = "https://github.com/rust-lang/cargo" }
 cargo-test-support = { git = "https://github.com/rust-lang/cargo" }
 dunce = "1.0.3"
 snapbox = { version = "0.4.3", features = ["cmd", "debug"] }
+zip.workspace = true
diff --git a/crates/cargo-lambda-deploy/src/lib.rs b/crates/cargo-lambda-deploy/src/lib.rs
index 7f2d4731..83f22bab 100644
--- a/crates/cargo-lambda-deploy/src/lib.rs
+++ b/crates/cargo-lambda-deploy/src/lib.rs
@@ -76,9 +76,18 @@ pub struct Deploy {
     #[arg(long)]
     extension: bool,
 
+    /// Whether an extension is internal or external
+    #[arg(long, requires = "extension")]
+    internal: bool,
+
     /// Comma separated list with compatible runtimes for the Lambda Extension (--compatible_runtimes=provided.al2,nodejs16.x)
     /// List of allowed runtimes can be found in the AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime
-    #[arg(long, value_delimiter = ',', default_value = "provided.al2")]
+    #[arg(
+        long,
+        value_delimiter = ',',
+        default_value = "provided.al2",
+        requires = "extension"
+    )]
     compatible_runtimes: Vec<String>,
 
     /// Format to render the output (text, or json)
@@ -204,7 +213,7 @@ impl Deploy {
                     .parent()
                     .ok_or_else(|| miette::miette!("invalid binary path {:?}", bp))?;
 
-                let parent = if self.extension {
+                let parent = if self.extension && !self.internal {
                     Some("extensions")
                 } else {
                     None
@@ -225,6 +234,7 @@ impl Deploy {
                     &self.manifest_path,
                     &self.lambda_dir,
                     self.extension,
+                    self.internal,
                 )?;
                 (name, arc)
             }
diff --git a/docs/commands/build.md b/docs/commands/build.md
index 94d4584d..73d0efa6 100644
--- a/docs/commands/build.md
+++ b/docs/commands/build.md
@@ -76,6 +76,12 @@ If you want to create a zip file with the structure that AWS Lambda expects to f
 cargo lambda build --release --extension --output-format zip
 ```
 
+If you're building an internal extension, add the `--internal` flag to the build command. You can skip this flag if you use `cargo lambda deploy` to deploy the extension later.
+
+```
+cargo lambda build --release --extension --internal --output-format zip
+```
+
 ## Compiler backends
 
 Cargo Lambda has an internal abstraction to work with different ways to compile functions.
diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md
index 6c2543cb..99e747eb 100644
--- a/docs/commands/deploy.md
+++ b/docs/commands/deploy.md
@@ -96,6 +96,15 @@ cargo lambda build --release --extension
 cargo lambda deploy --extension
 ```
 
+### Internal extensions
+
+To deploy an [internal extension](https://docs.aws.amazon.com/lambda/latest/dg/lambda-extensions.html), add the `--internal` flag to the deploy command:
+
+```
+cargo lambda build --release --extension
+cargo lambda deploy --extension --internal
+```
+
 ## Deploy configuration in Cargo's Metadata
 
 You can keep some deploy configuration options in your project's `Cargo.toml` file. This give you a more "configuration as code" approach since you can store that configuration along side your project. The following example shows the options that you can specify in the metadata, all of them are optional:


Test patch:
diff --git a/crates/cargo-lambda-cli/tests/cargo_lambda.rs b/crates/cargo-lambda-cli/tests/cargo_lambda.rs
index aab28eae..7727d503 100644
--- a/crates/cargo-lambda-cli/tests/cargo_lambda.rs
+++ b/crates/cargo-lambda-cli/tests/cargo_lambda.rs
@@ -3,6 +3,7 @@ use std::{
     fs::{create_dir_all, read_to_string, File},
     io::Write,
 };
+use zip::ZipArchive;
 
 mod harness;
 use harness::{
@@ -25,6 +26,38 @@ fn test_build_basic_function() {
     assert!(bin.exists(), "{:?} doesn't exist", bin);
 }
 
+#[cargo_test]
+fn test_build_basic_zip_function() {
+    let (root, cmd) = cargo_lambda_new("test-basic-function");
+
+    cmd.arg("--no-interactive")
+        .arg("test-basic-function")
+        .assert()
+        .success();
+
+    let project = test_project(root);
+    cargo_lambda_build(project.root())
+        .args(&["--output-format", "zip"])
+        .assert()
+        .success();
+
+    let bin = project
+        .lambda_dir()
+        .join("test-basic-function")
+        .join("bootstrap.zip");
+    assert!(bin.exists(), "{:?} doesn't exist", bin);
+    let file = File::open(bin).expect("failed to open zip file");
+    let mut zip = ZipArchive::new(file).expect("failed to initialize the zip archive");
+    assert!(
+        zip.by_name("bootstrap").is_ok(),
+        "bootstrap is not in the zip archive. Files in zip: {:?}",
+        zip.file_names()
+            .into_iter()
+            .collect::<Vec<&str>>()
+            .join(", ")
+    );
+}
+
 #[cargo_test]
 fn test_build_http_function() {
     let (root, cmd) = cargo_lambda_new("test-http-function");
@@ -145,3 +178,64 @@ fn test_init_subcommand_without_override() {
     let out = read_to_string(main).expect("failed to read main.rs file");
     assert_eq!(content, out);
 }
+
+#[cargo_test]
+fn test_build_basic_zip_extension() {
+    let (root, cmd) = cargo_lambda_new("test-basic-extension");
+
+    cmd.arg("--extension")
+        .arg("test-basic-extension")
+        .assert()
+        .success();
+
+    let project = test_project(root);
+    cargo_lambda_build(project.root())
+        .arg("--extension")
+        .args(&["--output-format", "zip"])
+        .assert()
+        .success();
+
+    let bin = project.lambda_extension_bin("test-basic-extension.zip");
+    assert!(bin.exists(), "{:?} doesn't exist", &bin);
+    let file = File::open(bin).expect("failed to open zip file");
+    let mut zip = ZipArchive::new(file).expect("failed to initialize the zip archive");
+    assert!(
+        zip.by_name("extensions/test-basic-extension").is_ok(),
+        "test-basic-extension is not in the zip archive. Files in zip: {:?}",
+        zip.file_names()
+            .into_iter()
+            .collect::<Vec<&str>>()
+            .join(", ")
+    );
+}
+
+#[cargo_test]
+fn test_build_internal_zip_extension() {
+    let (root, cmd) = cargo_lambda_new("test-internal-extension");
+
+    cmd.arg("--extension")
+        .arg("test-internal-extension")
+        .assert()
+        .success();
+
+    let project = test_project(root);
+    cargo_lambda_build(project.root())
+        .arg("--extension")
+        .arg("--internal")
+        .args(&["--output-format", "zip"])
+        .assert()
+        .success();
+
+    let bin = project.lambda_extension_bin("test-internal-extension.zip");
+    assert!(bin.exists(), "{:?} doesn't exist", &bin);
+    let file = File::open(bin).expect("failed to open zip file");
+    let mut zip = ZipArchive::new(file).expect("failed to initialize the zip archive");
+    assert!(
+        zip.by_name("test-internal-extension").is_ok(),
+        "test-internal-extension is not in the zip archive. Files in zip: {:?}",
+        zip.file_names()
+            .into_iter()
+            .collect::<Vec<&str>>()
+            .join(", ")
+    );
+}