Skip to main content

cargo_lambda_metadata/cargo/
build.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use cargo_options::Build as CargoBuild;
4use clap::{Args, ValueHint};
5use serde::{Deserialize, Serialize};
6use strum_macros::{Display, EnumString};
7
8use crate::{
9    cargo::{count_common_options, serialize_common_options},
10    env::{EnvOptions, Environment},
11    error::MetadataError,
12};
13
14#[derive(Args, Clone, Debug, Default, Deserialize)]
15#[command(
16    name = "build",
17    after_help = "Full command documentation: https://www.cargo-lambda.info/commands/build.html"
18)]
19pub struct Build {
20    /// The format to produce the compile Lambda into, acceptable values are [Binary, Zip]
21    #[arg(short, long)]
22    #[serde(default)]
23    pub output_format: Option<OutputFormat>,
24
25    /// Base directory where lambda binaries will be located (creates subdirectories per binary)
26    #[arg(short, long, value_hint = ValueHint::DirPath)]
27    #[serde(default)]
28    pub lambda_dir: Option<PathBuf>,
29
30    /// Shortcut for --target aarch64-unknown-linux-gnu
31    #[arg(long)]
32    #[serde(default)]
33    pub arm64: bool,
34
35    /// Shortcut for --target x86_64-unknown-linux-gnu
36    #[arg(long)]
37    #[serde(default)]
38    pub x86_64: bool,
39
40    /// Whether the code that you're building is a Lambda Extension
41    #[arg(long)]
42    #[serde(default)]
43    pub extension: bool,
44
45    /// Whether an extension is internal or external
46    #[arg(long, requires = "extension")]
47    #[serde(default)]
48    pub internal: bool,
49
50    /// Places the bootstrap file directly in the lambda directory instead of creating a subdirectory.
51    /// Specify the binary name to flatten.
52    #[arg(long)]
53    #[serde(default)]
54    pub flatten: Option<String>,
55
56    /// Whether to skip the target check
57    #[arg(long)]
58    #[serde(default)]
59    pub skip_target_check: bool,
60
61    /// Backend to build the project with
62    #[arg(short, long, env = "CARGO_LAMBDA_COMPILER")]
63    #[serde(default)]
64    pub compiler: Option<CompilerOptions>,
65
66    /// Disable all default release optimizations
67    #[arg(long)]
68    #[serde(default)]
69    pub disable_optimizations: bool,
70
71    /// Option to add one or more files and directories to include in the output ZIP file (only works with --output-format=zip).
72    #[arg(short, long)]
73    #[serde(default)]
74    pub include: Option<Vec<String>>,
75
76    #[command(flatten)]
77    #[serde(default, flatten)]
78    pub env_options: EnvOptions,
79
80    #[command(flatten)]
81    #[serde(default, flatten)]
82    pub cargo_opts: CargoBuild,
83}
84
85#[derive(Clone, Debug, Default, Deserialize, Display, EnumString, PartialEq, Serialize)]
86#[strum(ascii_case_insensitive)]
87#[serde(rename_all = "snake_case")]
88pub enum OutputFormat {
89    #[default]
90    Binary,
91    Zip,
92}
93
94#[derive(Clone, Debug, Default, Deserialize, Display, Eq, PartialEq, Serialize)]
95#[serde(tag = "type", rename_all = "snake_case")]
96pub enum CompilerOptions {
97    #[default]
98    CargoZigbuild,
99    Cargo(CargoCompilerOptions),
100    Cross,
101}
102
103impl From<String> for CompilerOptions {
104    fn from(s: String) -> Self {
105        match s.to_lowercase().as_str() {
106            "cargo" => Self::Cargo(CargoCompilerOptions::default()),
107            "cross" => Self::Cross,
108            _ => Self::CargoZigbuild,
109        }
110    }
111}
112
113impl CompilerOptions {
114    pub fn is_local_cargo(&self) -> bool {
115        matches!(self, CompilerOptions::Cargo(_))
116    }
117
118    pub fn is_cargo_zigbuild(&self) -> bool {
119        matches!(self, CompilerOptions::CargoZigbuild)
120    }
121}
122
123#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
124pub struct CargoCompilerOptions {
125    #[serde(default)]
126    pub subcommand: Option<Vec<String>>,
127    #[serde(default)]
128    pub extra_args: Option<Vec<String>>,
129}
130
131impl Build {
132    pub fn manifest_path(&self) -> PathBuf {
133        self.cargo_opts
134            .manifest_path
135            .clone()
136            .unwrap_or_else(|| "Cargo.toml".into())
137    }
138
139    pub fn output_format(&self) -> &OutputFormat {
140        self.output_format.as_ref().unwrap_or(&OutputFormat::Binary)
141    }
142
143    /// Returns the package name if there is only one package in the list of `packages`,
144    /// otherwise None.
145    pub fn pkg_name(&self) -> Option<String> {
146        if self.cargo_opts.packages.len() > 1 {
147            return None;
148        }
149        self.cargo_opts.packages.first().map(|s| s.to_string())
150    }
151
152    pub fn bin_name(&self) -> Option<String> {
153        if self.cargo_opts.bin.len() > 1 {
154            return None;
155        }
156        self.cargo_opts.bin.first().map(|s| s.to_string())
157    }
158
159    pub fn build_environment(&self) -> Result<Environment, MetadataError> {
160        self.env_options.lambda_environment(&HashMap::new())
161    }
162}
163
164impl Serialize for Build {
165    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166    where
167        S: serde::Serializer,
168    {
169        use serde::ser::SerializeStruct;
170
171        // Count how many fields we'll actually serialize
172        let field_count = self.output_format.is_some() as usize
173            + self.lambda_dir.is_some() as usize
174            + self.flatten.is_some() as usize
175            + self.compiler.is_some() as usize
176            + self.include.is_some() as usize
177            + self.arm64 as usize
178            + self.x86_64 as usize
179            + self.extension as usize
180            + self.internal as usize
181            + self.skip_target_check as usize
182            + self.disable_optimizations as usize
183            + self.cargo_opts.manifest_path.is_some() as usize
184            + self.cargo_opts.bins as usize
185            + !self.cargo_opts.bin.is_empty() as usize
186            + self.cargo_opts.examples as usize
187            + !self.cargo_opts.example.is_empty() as usize
188            + self.cargo_opts.all_targets as usize
189            + !self.cargo_opts.packages.is_empty() as usize
190            + self.cargo_opts.workspace as usize
191            + !self.cargo_opts.exclude.is_empty() as usize
192            + self.cargo_opts.tests as usize
193            + !self.cargo_opts.test.is_empty() as usize
194            + self.cargo_opts.benches as usize
195            + !self.cargo_opts.bench.is_empty() as usize
196            + count_common_options(&self.cargo_opts.common)
197            + self.env_options.count_fields();
198
199        let mut state = serializer.serialize_struct("Build", field_count)?;
200
201        // Optional fields
202        if let Some(ref output_format) = self.output_format {
203            state.serialize_field("output_format", output_format)?;
204        }
205        if let Some(ref lambda_dir) = self.lambda_dir {
206            state.serialize_field("lambda_dir", lambda_dir)?;
207        }
208        if let Some(ref flatten) = self.flatten {
209            state.serialize_field("flatten", flatten)?;
210        }
211        if let Some(ref compiler) = self.compiler {
212            state.serialize_field("compiler", compiler)?;
213        }
214        if let Some(ref include) = self.include {
215            state.serialize_field("include", include)?;
216        }
217
218        // Boolean fields
219        if self.arm64 {
220            state.serialize_field("arm64", &true)?;
221        }
222        if self.x86_64 {
223            state.serialize_field("x86_64", &true)?;
224        }
225        if self.extension {
226            state.serialize_field("extension", &true)?;
227        }
228        if self.internal {
229            state.serialize_field("internal", &true)?;
230        }
231        if self.skip_target_check {
232            state.serialize_field("skip_target_check", &true)?;
233        }
234        if self.disable_optimizations {
235            state.serialize_field("disable_optimizations", &true)?;
236        }
237
238        // Environment options
239        self.env_options.serialize_fields::<S>(&mut state)?;
240
241        // Cargo opts fields
242        if let Some(ref manifest_path) = self.cargo_opts.manifest_path {
243            state.serialize_field("manifest_path", manifest_path)?;
244        }
245        if self.cargo_opts.release {
246            state.serialize_field("release", &true)?;
247        }
248        if self.cargo_opts.bins {
249            state.serialize_field("bins", &true)?;
250        }
251        if !self.cargo_opts.bin.is_empty() {
252            state.serialize_field("bin", &self.cargo_opts.bin)?;
253        }
254        if self.cargo_opts.examples {
255            state.serialize_field("examples", &true)?;
256        }
257        if !self.cargo_opts.example.is_empty() {
258            state.serialize_field("example", &self.cargo_opts.example)?;
259        }
260        if self.cargo_opts.all_targets {
261            state.serialize_field("all_targets", &true)?;
262        }
263        if !self.cargo_opts.packages.is_empty() {
264            state.serialize_field("packages", &self.cargo_opts.packages)?;
265        }
266        if self.cargo_opts.workspace {
267            state.serialize_field("workspace", &true)?;
268        }
269        if !self.cargo_opts.exclude.is_empty() {
270            state.serialize_field("exclude", &self.cargo_opts.exclude)?;
271        }
272        if self.cargo_opts.tests {
273            state.serialize_field("tests", &true)?;
274        }
275        if !self.cargo_opts.test.is_empty() {
276            state.serialize_field("test", &self.cargo_opts.test)?;
277        }
278        if self.cargo_opts.benches {
279            state.serialize_field("benches", &true)?;
280        }
281        if !self.cargo_opts.bench.is_empty() {
282            state.serialize_field("bench", &self.cargo_opts.bench)?;
283        }
284        serialize_common_options::<S>(&mut state, &self.cargo_opts.common)?;
285
286        state.end()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use cargo_options::CommonOptions;
294    use serde_json::json;
295
296    #[test]
297    fn test_serialize_minimal_build() {
298        let build = Build::default();
299        let serialized = serde_json::to_value(&build).unwrap();
300
301        assert_eq!(serialized, json!({}));
302    }
303
304    #[test]
305    fn test_serialize_with_optional_fields() {
306        let build = Build {
307            lambda_dir: Some(PathBuf::from("/tmp/lambda")),
308            compiler: Some(CompilerOptions::Cross),
309            include: Some(vec!["file1.txt".to_string(), "file2.txt".to_string()]),
310            ..Default::default()
311        };
312
313        let serialized = serde_json::to_value(&build).unwrap();
314
315        assert_eq!(
316            serialized,
317            json!({
318                "lambda_dir": "/tmp/lambda",
319                "compiler": { "type": "cross" },
320                "include": ["file1.txt", "file2.txt"]
321            })
322        );
323    }
324
325    #[test]
326    fn test_serialize_with_boolean_fields() {
327        let build = Build {
328            arm64: true,
329            extension: true,
330            skip_target_check: true,
331            ..Default::default()
332        };
333
334        let serialized = serde_json::to_value(&build).unwrap();
335
336        assert_eq!(
337            serialized,
338            json!({
339                "arm64": true,
340                "extension": true,
341                "skip_target_check": true
342            })
343        );
344    }
345
346    #[test]
347    fn test_serialize_with_cargo_opts() {
348        let build = Build {
349            cargo_opts: CargoBuild {
350                common: CommonOptions {
351                    target: vec!["x86_64-unknown-linux-gnu".to_string()],
352                    features: vec!["feature1".to_string(), "feature2".to_string()],
353                    all_features: true,
354                    profile: Some("release".to_string()),
355                    ..Default::default()
356                },
357                ..Default::default()
358            },
359            ..Default::default()
360        };
361
362        let serialized = serde_json::to_value(&build).unwrap();
363
364        assert_eq!(
365            serialized,
366            json!({
367                "target": ["x86_64-unknown-linux-gnu"],
368                "features": ["feature1", "feature2"],
369                "all_features": true,
370                "profile": "release"
371            })
372        );
373    }
374
375    #[test]
376    fn test_serialize_complete_build() {
377        let build = Build {
378            // Main struct fields
379            output_format: Some(OutputFormat::Zip),
380            lambda_dir: Some(PathBuf::from("/tmp/lambda")),
381            arm64: true,
382            extension: true,
383            compiler: Some(CompilerOptions::CargoZigbuild),
384            include: Some(vec!["include1".to_string()]),
385
386            // Cargo opts
387            cargo_opts: CargoBuild {
388                common: CommonOptions {
389                    target: vec!["x86_64-unknown-linux-gnu".to_string()],
390                    features: vec!["feature1".to_string()],
391                    all_features: true,
392                    ..Default::default()
393                },
394                ..Default::default()
395            },
396            ..Default::default()
397        };
398
399        let serialized = serde_json::to_value(&build).unwrap();
400
401        assert_eq!(
402            serialized,
403            json!({
404                "output_format": "zip",
405                "lambda_dir": "/tmp/lambda",
406                "arm64": true,
407                "extension": true,
408                "compiler": { "type": "cargo_zigbuild" },
409                "include": ["include1"],
410                "target": ["x86_64-unknown-linux-gnu"],
411                "features": ["feature1"],
412                "all_features": true
413            })
414        );
415    }
416
417    #[test]
418    fn test_deserialize_with_env_var() {
419        let config = json!({
420            "env_var": ["KEY1=VALUE1", "KEY2=VALUE2"]
421        });
422        let build: Build = serde_json::from_value(config).unwrap();
423        assert_eq!(
424            build.env_options.env_var,
425            Some(vec!["KEY1=VALUE1".to_string(), "KEY2=VALUE2".to_string()])
426        );
427    }
428
429    #[test]
430    fn test_deserialize_with_env_file() {
431        let config = json!({
432            "env_file": "/tmp/.env"
433        });
434        let build: Build = serde_json::from_value(config).unwrap();
435        assert_eq!(build.env_options.env_file, Some(PathBuf::from("/tmp/.env")));
436    }
437
438    #[test]
439    fn test_serialize_with_env_options() {
440        let build = Build {
441            env_options: EnvOptions {
442                env_var: Some(vec!["FOO=BAR".to_string()]),
443                ..Default::default()
444            },
445            ..Default::default()
446        };
447        let serialized = serde_json::to_value(&build).unwrap();
448        assert_eq!(serialized["env_var"], json!(["FOO=BAR"]));
449    }
450
451    #[test]
452    fn test_build_environment() {
453        let build = Build {
454            env_options: EnvOptions {
455                env_var: Some(vec!["KEY=VALUE".to_string()]),
456                ..Default::default()
457            },
458            ..Default::default()
459        };
460        let env = build.build_environment().unwrap();
461        assert_eq!(env.get("KEY").unwrap(), "VALUE");
462    }
463}