cargo_lambda_metadata/cargo/
build.rs1use 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 #[arg(short, long)]
22 #[serde(default)]
23 pub output_format: Option<OutputFormat>,
24
25 #[arg(short, long, value_hint = ValueHint::DirPath)]
27 #[serde(default)]
28 pub lambda_dir: Option<PathBuf>,
29
30 #[arg(long)]
32 #[serde(default)]
33 pub arm64: bool,
34
35 #[arg(long)]
37 #[serde(default)]
38 pub x86_64: bool,
39
40 #[arg(long)]
42 #[serde(default)]
43 pub extension: bool,
44
45 #[arg(long, requires = "extension")]
47 #[serde(default)]
48 pub internal: bool,
49
50 #[arg(long)]
53 #[serde(default)]
54 pub flatten: Option<String>,
55
56 #[arg(long)]
58 #[serde(default)]
59 pub skip_target_check: bool,
60
61 #[arg(short, long, env = "CARGO_LAMBDA_COMPILER")]
63 #[serde(default)]
64 pub compiler: Option<CompilerOptions>,
65
66 #[arg(long)]
68 #[serde(default)]
69 pub disable_optimizations: bool,
70
71 #[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 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 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 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 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 self.env_options.serialize_fields::<S>(&mut state)?;
240
241 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 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: 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}