cargo_lambda_metadata/cargo/
build.rs

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