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
136impl Serialize for Build {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::Serializer,
140    {
141        use serde::ser::SerializeStruct;
142
143        // Count how many fields we'll actually serialize
144        let field_count = self.output_format.is_some() as usize
145            + self.lambda_dir.is_some() as usize
146            + self.flatten.is_some() as usize
147            + self.compiler.is_some() as usize
148            + self.include.is_some() as usize
149            + self.arm64 as usize
150            + self.x86_64 as usize
151            + self.extension as usize
152            + self.internal as usize
153            + self.skip_target_check as usize
154            + self.disable_optimizations as usize
155            + self.cargo_opts.manifest_path.is_some() as usize
156            + self.cargo_opts.bins as usize
157            + !self.cargo_opts.bin.is_empty() as usize
158            + self.cargo_opts.examples as usize
159            + !self.cargo_opts.example.is_empty() as usize
160            + self.cargo_opts.all_targets as usize
161            + !self.cargo_opts.packages.is_empty() as usize
162            + self.cargo_opts.workspace as usize
163            + !self.cargo_opts.exclude.is_empty() as usize
164            + self.cargo_opts.tests as usize
165            + !self.cargo_opts.test.is_empty() as usize
166            + self.cargo_opts.benches as usize
167            + !self.cargo_opts.bench.is_empty() as usize
168            + count_common_options(&self.cargo_opts.common);
169
170        let mut state = serializer.serialize_struct("Build", field_count)?;
171
172        // Optional fields
173        if let Some(ref output_format) = self.output_format {
174            state.serialize_field("output_format", output_format)?;
175        }
176        if let Some(ref lambda_dir) = self.lambda_dir {
177            state.serialize_field("lambda_dir", lambda_dir)?;
178        }
179        if let Some(ref flatten) = self.flatten {
180            state.serialize_field("flatten", flatten)?;
181        }
182        if let Some(ref compiler) = self.compiler {
183            state.serialize_field("compiler", compiler)?;
184        }
185        if let Some(ref include) = self.include {
186            state.serialize_field("include", include)?;
187        }
188
189        // Boolean fields
190        if self.arm64 {
191            state.serialize_field("arm64", &true)?;
192        }
193        if self.x86_64 {
194            state.serialize_field("x86_64", &true)?;
195        }
196        if self.extension {
197            state.serialize_field("extension", &true)?;
198        }
199        if self.internal {
200            state.serialize_field("internal", &true)?;
201        }
202        if self.skip_target_check {
203            state.serialize_field("skip_target_check", &true)?;
204        }
205        if self.disable_optimizations {
206            state.serialize_field("disable_optimizations", &true)?;
207        }
208
209        // Cargo opts fields
210        if let Some(ref manifest_path) = self.cargo_opts.manifest_path {
211            state.serialize_field("manifest_path", manifest_path)?;
212        }
213        if self.cargo_opts.release {
214            state.serialize_field("release", &true)?;
215        }
216        if self.cargo_opts.bins {
217            state.serialize_field("bins", &true)?;
218        }
219        if !self.cargo_opts.bin.is_empty() {
220            state.serialize_field("bin", &self.cargo_opts.bin)?;
221        }
222        if self.cargo_opts.examples {
223            state.serialize_field("examples", &true)?;
224        }
225        if !self.cargo_opts.example.is_empty() {
226            state.serialize_field("example", &self.cargo_opts.example)?;
227        }
228        if self.cargo_opts.all_targets {
229            state.serialize_field("all_targets", &true)?;
230        }
231        if !self.cargo_opts.packages.is_empty() {
232            state.serialize_field("packages", &self.cargo_opts.packages)?;
233        }
234        if self.cargo_opts.workspace {
235            state.serialize_field("workspace", &true)?;
236        }
237        if !self.cargo_opts.exclude.is_empty() {
238            state.serialize_field("exclude", &self.cargo_opts.exclude)?;
239        }
240        if self.cargo_opts.tests {
241            state.serialize_field("tests", &true)?;
242        }
243        if !self.cargo_opts.test.is_empty() {
244            state.serialize_field("test", &self.cargo_opts.test)?;
245        }
246        if self.cargo_opts.benches {
247            state.serialize_field("benches", &true)?;
248        }
249        if !self.cargo_opts.bench.is_empty() {
250            state.serialize_field("bench", &self.cargo_opts.bench)?;
251        }
252        serialize_common_options::<S>(&mut state, &self.cargo_opts.common)?;
253
254        state.end()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use cargo_options::CommonOptions;
262    use serde_json::json;
263
264    #[test]
265    fn test_serialize_minimal_build() {
266        let build = Build::default();
267        let serialized = serde_json::to_value(&build).unwrap();
268
269        assert_eq!(serialized, json!({}));
270    }
271
272    #[test]
273    fn test_serialize_with_optional_fields() {
274        let build = Build {
275            lambda_dir: Some(PathBuf::from("/tmp/lambda")),
276            compiler: Some(CompilerOptions::Cross),
277            include: Some(vec!["file1.txt".to_string(), "file2.txt".to_string()]),
278            ..Default::default()
279        };
280
281        let serialized = serde_json::to_value(&build).unwrap();
282
283        assert_eq!(
284            serialized,
285            json!({
286                "lambda_dir": "/tmp/lambda",
287                "compiler": { "type": "cross" },
288                "include": ["file1.txt", "file2.txt"]
289            })
290        );
291    }
292
293    #[test]
294    fn test_serialize_with_boolean_fields() {
295        let build = Build {
296            arm64: true,
297            extension: true,
298            skip_target_check: true,
299            ..Default::default()
300        };
301
302        let serialized = serde_json::to_value(&build).unwrap();
303
304        assert_eq!(
305            serialized,
306            json!({
307                "arm64": true,
308                "extension": true,
309                "skip_target_check": true
310            })
311        );
312    }
313
314    #[test]
315    fn test_serialize_with_cargo_opts() {
316        let build = Build {
317            cargo_opts: CargoBuild {
318                common: CommonOptions {
319                    target: vec!["x86_64-unknown-linux-gnu".to_string()],
320                    features: vec!["feature1".to_string(), "feature2".to_string()],
321                    all_features: true,
322                    profile: Some("release".to_string()),
323                    ..Default::default()
324                },
325                ..Default::default()
326            },
327            ..Default::default()
328        };
329
330        let serialized = serde_json::to_value(&build).unwrap();
331
332        assert_eq!(
333            serialized,
334            json!({
335                "target": ["x86_64-unknown-linux-gnu"],
336                "features": ["feature1", "feature2"],
337                "all_features": true,
338                "profile": "release"
339            })
340        );
341    }
342
343    #[test]
344    fn test_serialize_complete_build() {
345        let build = Build {
346            // Main struct fields
347            output_format: Some(OutputFormat::Zip),
348            lambda_dir: Some(PathBuf::from("/tmp/lambda")),
349            arm64: true,
350            extension: true,
351            compiler: Some(CompilerOptions::CargoZigbuild),
352            include: Some(vec!["include1".to_string()]),
353
354            // Cargo opts
355            cargo_opts: CargoBuild {
356                common: CommonOptions {
357                    target: vec!["x86_64-unknown-linux-gnu".to_string()],
358                    features: vec!["feature1".to_string()],
359                    all_features: true,
360                    ..Default::default()
361                },
362                ..Default::default()
363            },
364            ..Default::default()
365        };
366
367        let serialized = serde_json::to_value(&build).unwrap();
368
369        assert_eq!(
370            serialized,
371            json!({
372                "output_format": "zip",
373                "lambda_dir": "/tmp/lambda",
374                "arm64": true,
375                "extension": true,
376                "compiler": { "type": "cargo_zigbuild" },
377                "include": ["include1"],
378                "target": ["x86_64-unknown-linux-gnu"],
379                "features": ["feature1"],
380                "all_features": true
381            })
382        );
383    }
384}