cargo_lambda_metadata/cargo/
mod.rs1pub use cargo_metadata::{
2 Metadata as CargoMetadata, Package as CargoPackage, Target as CargoTarget,
3};
4use cargo_options::CommonOptions;
5use miette::Result;
6use serde::{Deserialize, Serialize, ser::SerializeStruct};
7use serde_json::Value;
8use std::{
9 collections::{HashMap, HashSet},
10 fmt::Debug,
11 fs::{metadata, read_to_string},
12 path::{Path, PathBuf},
13};
14use tracing::{Level, enabled, trace};
15
16use crate::error::MetadataError;
17
18pub mod build;
19use build::Build;
20
21pub mod deploy;
22use deploy::Deploy;
23
24pub mod profile;
25use profile::CargoProfile;
26
27pub mod watch;
28use watch::Watch;
29const STRIP_CONFIG: &str = "profile.release.strip=\"symbols\"";
30const LTO_CONFIG: &str = "profile.release.lto=\"thin\"";
31const CODEGEN_CONFIG: &str = "profile.release.codegen-units=1";
32const PANIC_CONFIG: &str = "profile.release.panic=\"abort\"";
33
34#[derive(Debug, Default, Deserialize, Serialize)]
35#[non_exhaustive]
36pub struct Metadata {
37 #[serde(default)]
38 pub lambda: LambdaMetadata,
39 #[serde(default)]
40 profile: Option<CargoProfile>,
41}
42
43#[derive(Clone, Debug, Default, Deserialize, Serialize)]
44#[non_exhaustive]
45pub struct LambdaMetadata {
46 #[serde(flatten)]
47 pub package: PackageMetadata,
48 #[serde(default)]
49 pub bin: HashMap<String, PackageMetadata>,
50}
51
52#[derive(Clone, Debug, Default, Deserialize, Serialize)]
53#[non_exhaustive]
54pub struct PackageMetadata {
55 #[serde(default)]
56 pub env: HashMap<String, String>,
57 #[serde(default)]
58 pub deploy: Option<Deploy>,
59 #[serde(default)]
60 pub build: Option<Build>,
61 #[serde(default)]
62 pub watch: Option<Watch>,
63}
64
65pub fn binary_targets<P: AsRef<Path> + Debug>(
67 manifest_path: P,
68 build_examples: bool,
69) -> Result<HashSet<String>, MetadataError> {
70 let metadata = load_metadata(manifest_path, None)?;
71 Ok(binary_targets_from_metadata(&metadata, build_examples))
72}
73
74pub fn binary_targets_from_metadata(
75 metadata: &CargoMetadata,
76 build_examples: bool,
77) -> HashSet<String> {
78 let condition = if build_examples {
79 kind_example_filter
80 } else {
81 kind_bin_filter
82 };
83
84 let package_filter: Option<fn(&&CargoPackage) -> bool> = None;
85 filter_binary_targets_from_metadata(metadata, condition, package_filter)
86}
87
88pub fn kind_bin_filter(target: &CargoTarget) -> bool {
89 target.kind.iter().any(|k| k == "bin")
90}
91
92pub fn selected_bin_filter(selected_bins: Vec<String>) -> Box<dyn Fn(&CargoTarget) -> bool> {
93 let bins: HashSet<String> = selected_bins.into_iter().collect();
94 Box::new(move |t: &CargoTarget| kind_bin_filter(t) && bins.contains(&t.name))
95}
96
97pub fn kind_example_filter(target: &CargoTarget) -> bool {
101 target.kind.iter().any(|k| k == "example") && target.crate_types.iter().any(|t| t == "bin")
102}
103
104pub fn filter_binary_targets<P, F, K>(
106 manifest_path: P,
107 target_filter: F,
108 package_filter: Option<K>,
109) -> Result<HashSet<String>, MetadataError>
110where
111 P: AsRef<Path> + Debug,
112 F: FnMut(&CargoTarget) -> bool,
113 K: FnMut(&&CargoPackage) -> bool,
114{
115 let metadata = load_metadata(manifest_path, None)?;
116 Ok(filter_binary_targets_from_metadata(
117 &metadata,
118 target_filter,
119 package_filter,
120 ))
121}
122
123pub fn filter_binary_targets_from_metadata<F, P>(
124 metadata: &CargoMetadata,
125 target_filter: F,
126 package_filter: Option<P>,
127) -> HashSet<String>
128where
129 F: FnMut(&CargoTarget) -> bool,
130 P: FnMut(&&CargoPackage) -> bool,
131{
132 let packages = metadata.packages.iter();
133 let targets = if let Some(filter) = package_filter {
134 packages
135 .filter(filter)
136 .flat_map(|p| p.targets.clone())
137 .collect::<Vec<_>>()
138 } else {
139 packages.flat_map(|p| p.targets.clone()).collect::<Vec<_>>()
140 };
141
142 targets
143 .into_iter()
144 .filter(target_filter)
145 .map(|target| target.name.clone())
146 .collect::<_>()
147}
148
149pub fn target_dir_from_metadata(metadata: &CargoMetadata) -> Result<PathBuf> {
155 Ok(metadata.target_directory.clone().into_std_path_buf())
156}
157
158pub fn cargo_release_profile_config<'a>(
162 metadata: &CargoMetadata,
163) -> Result<HashSet<&'a str>, MetadataError> {
164 let path = metadata.workspace_root.join("Cargo.toml");
165 let file =
166 read_to_string(&path).map_err(|e| MetadataError::InvalidManifestFile(path.into(), e))?;
167
168 let metadata: Metadata = toml::from_str(&file).map_err(MetadataError::InvalidTomlManifest)?;
169
170 Ok(cargo_release_profile_config_from_metadata(metadata))
171}
172
173fn cargo_release_profile_config_from_metadata(metadata: Metadata) -> HashSet<&'static str> {
174 let mut config = HashSet::from([STRIP_CONFIG, LTO_CONFIG, CODEGEN_CONFIG, PANIC_CONFIG]);
175
176 let Some(profile) = &metadata.profile else {
177 return config;
178 };
179 let Some(release) = &profile.release else {
180 return config;
181 };
182
183 if release.strip.is_some() || release.debug_enabled() {
184 config.remove(STRIP_CONFIG);
185 }
186 if release.lto.is_some() {
187 config.remove(LTO_CONFIG);
188 }
189 if release.codegen_units.is_some() {
190 config.remove(CODEGEN_CONFIG);
191 }
192 if release.panic.is_some() {
193 config.remove(PANIC_CONFIG);
194 }
195
196 config
197}
198
199#[tracing::instrument(target = "cargo_lambda")]
201pub fn load_metadata<P: AsRef<Path> + Debug>(
202 manifest_path: P,
203 target_dir: Option<&Path>,
204) -> Result<CargoMetadata, MetadataError> {
205 trace!("loading Cargo metadata");
206 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
207 metadata_cmd
208 .no_deps()
209 .verbose(enabled!(target: "cargo_lambda", Level::TRACE));
210
211 if let Some(dir) = target_dir {
213 metadata_cmd.other_options(vec![format!("--target-dir={}", dir.display())]);
214 }
215
216 let manifest_ref = manifest_path.as_ref();
219
220 match (manifest_ref.parent(), manifest_ref.file_name()) {
221 (Some(project), Some(manifest)) if is_project_metadata_ok(project) => {
222 metadata_cmd.current_dir(project);
223 metadata_cmd.manifest_path(manifest);
224 }
225 _ => {
226 metadata_cmd.manifest_path(manifest_ref);
229 }
230 }
231
232 trace!(metadata = ?metadata_cmd, "loading cargo metadata");
233 let meta = metadata_cmd
234 .exec()
235 .map_err(MetadataError::FailedCmdExecution)?;
236 trace!(metadata = ?meta, "loaded cargo metadata");
237 Ok(meta)
238}
239
240pub fn main_binary_from_metadata(metadata: &CargoMetadata) -> Result<String, MetadataError> {
245 let targets = binary_targets_from_metadata(metadata, false);
246 if targets.len() > 1 {
247 let mut vec = targets.into_iter().collect::<Vec<_>>();
248 vec.sort();
249 Err(MetadataError::MultipleBinariesInProject(vec.join(", ")))
250 } else if targets.is_empty() {
251 Err(MetadataError::MissingBinaryInProject)
252 } else {
253 targets
254 .into_iter()
255 .next()
256 .ok_or(MetadataError::MissingBinaryInProject)
257 }
258}
259
260fn is_project_metadata_ok(path: &Path) -> bool {
261 path.is_dir() && metadata(path).is_ok()
262}
263
264pub(crate) fn serialize_common_options<S>(
265 state: &mut <S as serde::Serializer>::SerializeStruct,
266 opts: &CommonOptions,
267) -> Result<(), S::Error>
268where
269 S: serde::Serializer,
270{
271 if opts.quiet {
272 state.serialize_field("quiet", &true)?;
273 }
274 if let Some(jobs) = opts.jobs {
275 state.serialize_field("jobs", &jobs)?;
276 }
277 if opts.keep_going {
278 state.serialize_field("keep_going", &true)?;
279 }
280 if let Some(profile) = &opts.profile {
281 state.serialize_field("profile", profile)?;
282 }
283 if !opts.features.is_empty() {
284 state.serialize_field("features", &opts.features)?;
285 }
286 if opts.all_features {
287 state.serialize_field("all_features", &true)?;
288 }
289 if opts.no_default_features {
290 state.serialize_field("no_default_features", &true)?;
291 }
292 if !opts.target.is_empty() {
293 state.serialize_field("target", &opts.target)?;
294 }
295 if let Some(target_dir) = &opts.target_dir {
296 state.serialize_field("target_dir", target_dir)?;
297 }
298 if !opts.message_format.is_empty() {
299 state.serialize_field("message_format", &opts.message_format)?;
300 }
301 if opts.verbose > 0 {
302 state.serialize_field("verbose", &opts.verbose)?;
303 }
304 if let Some(color) = &opts.color {
305 state.serialize_field("color", color)?;
306 }
307 if opts.frozen {
308 state.serialize_field("frozen", &true)?;
309 }
310 if opts.locked {
311 state.serialize_field("locked", &true)?;
312 }
313 if opts.offline {
314 state.serialize_field("offline", &true)?;
315 }
316 if !opts.config.is_empty() {
317 state.serialize_field("config", &opts.config)?;
318 }
319 if !opts.unstable_flags.is_empty() {
320 state.serialize_field("unstable_flags", &opts.unstable_flags)?;
321 }
322 if let Some(timings) = &opts.timings {
323 state.serialize_field("timings", timings)?;
324 }
325
326 Ok(())
327}
328
329pub(crate) fn count_common_options(opts: &CommonOptions) -> usize {
330 opts.quiet as usize
331 + opts.jobs.is_some() as usize
332 + opts.keep_going as usize
333 + opts.profile.is_some() as usize
334 + !opts.features.is_empty() as usize
335 + opts.all_features as usize
336 + opts.no_default_features as usize
337 + !opts.target.is_empty() as usize
338 + opts.target_dir.is_some() as usize
339 + !opts.message_format.is_empty() as usize
340 + (opts.verbose > 0) as usize
341 + opts.color.is_some() as usize
342 + opts.frozen as usize
343 + opts.locked as usize
344 + opts.offline as usize
345 + !opts.config.is_empty() as usize
346 + !opts.unstable_flags.is_empty() as usize
347 + opts.timings.is_some() as usize
348}
349
350pub(crate) fn deserialize_vec_or_map<'de, D>(
351 deserializer: D,
352) -> Result<Option<Vec<String>>, D::Error>
353where
354 D: serde::Deserializer<'de>,
355{
356 let value = Value::deserialize(deserializer)?;
357
358 match value {
359 Value::Array(arr) => {
360 let el = arr
361 .into_iter()
362 .map(|v| v.as_str().map(String::from))
363 .collect::<Option<Vec<_>>>();
364 Ok(el)
365 }
366 Value::Object(map) => {
367 let el = map
368 .into_iter()
369 .map(|(k, v)| format!("{}={}", k, v.as_str().unwrap_or("")))
370 .collect();
371 Ok(Some(el))
372 }
373 _ => Ok(None),
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use crate::tests::fixture_metadata;
380
381 use super::*;
382
383 #[test]
384 fn test_binary_packages() {
385 let bins = binary_targets(fixture_metadata("single-binary-package"), false).unwrap();
386 assert_eq!(1, bins.len());
387 assert!(bins.contains("basic-lambda"));
388 }
389
390 #[test]
391 fn test_binary_packages_with_mutiple_bin_entries() {
392 let bins = binary_targets(fixture_metadata("multi-binary-package"), false).unwrap();
393 assert_eq!(5, bins.len());
394 assert!(bins.contains("delete-product"));
395 assert!(bins.contains("get-product"));
396 assert!(bins.contains("get-products"));
397 assert!(bins.contains("put-product"));
398 assert!(bins.contains("dynamodb-streams"));
399 }
400
401 #[test]
402 fn test_binary_packages_with_workspace() {
403 let bins = binary_targets(fixture_metadata("workspace-package"), false).unwrap();
404 assert_eq!(3, bins.len());
405 assert!(bins.contains("basic-lambda-1"));
406 assert!(bins.contains("basic-lambda-2"));
407 assert!(bins.contains("crate-3"));
408 }
409
410 #[test]
411 fn test_binary_packages_with_mixed_workspace() {
412 let bins = binary_targets(fixture_metadata("mixed-workspace-package"), false).unwrap();
413 assert_eq!(1, bins.len());
414 assert!(bins.contains("function-crate"), "{bins:?}");
415 }
416
417 #[test]
418 fn test_binary_packages_with_missing_binary_info() {
419 let err = binary_targets(fixture_metadata("missing-binary-package"), false).unwrap_err();
420 assert!(
421 err.to_string()
422 .contains("a [lib] section, or [[bin]] section must be present")
423 );
424 }
425
426 #[test]
427 fn test_main_binary_with_package_name() {
428 let manifest_path = fixture_metadata("single-binary-package");
429 let metadata = load_metadata(manifest_path, None).unwrap();
430 let name = main_binary_from_metadata(&metadata).unwrap();
431 assert_eq!("basic-lambda", name);
432 }
433
434 #[test]
435 fn test_main_binary_with_binary_name() {
436 let manifest_path = fixture_metadata("single-binary-different-name");
437 let metadata = load_metadata(manifest_path, None).unwrap();
438 let name = main_binary_from_metadata(&metadata).unwrap();
439 assert_eq!("basic-lambda-binary", name);
440 }
441
442 #[test]
443 fn test_main_binary_multi_binaries() {
444 let manifest_path = fixture_metadata("multi-binary-package");
445 let metadata = load_metadata(manifest_path, None).unwrap();
446 let err = main_binary_from_metadata(&metadata).unwrap_err();
447 assert_eq!(
448 "there are more than one binary in the project, please specify a binary name with --binary-name or --binary-path. This is the list of binaries I found: delete-product, dynamodb-streams, get-product, get-products, put-product",
449 err.to_string()
450 );
451 }
452
453 #[test]
454 fn test_select_binary() {
455 let manifest_path = fixture_metadata("multi-binary-package");
456 let metadata = load_metadata(manifest_path, None).unwrap();
457
458 let package_filter: Option<fn(&&CargoPackage) -> bool> = None;
459
460 let bin = "delete-product".to_string();
461 let binary_filter = selected_bin_filter(vec![bin.clone()]);
462
463 let binaries =
464 filter_binary_targets_from_metadata(&metadata, binary_filter, package_filter);
465
466 assert_eq!(1, binaries.len());
467 assert!(binaries.contains(&bin));
468 }
469
470 #[test]
471 fn test_example_packages() {
472 let bins = binary_targets(fixture_metadata("examples-package"), true).unwrap();
473 assert_eq!(1, bins.len());
474 assert!(bins.contains("example-lambda"));
475 }
476
477 #[test]
478 fn test_release_config() {
479 let config = cargo_release_profile_config_from_metadata(Metadata::default());
480 assert!(config.contains(STRIP_CONFIG));
481 assert!(config.contains(LTO_CONFIG));
482 assert!(config.contains(CODEGEN_CONFIG));
483 assert!(config.contains(PANIC_CONFIG));
484 }
485
486 #[test]
487 fn test_release_config_with_workspace() {
488 let metadata = load_metadata(fixture_metadata("workspace-package"), None).unwrap();
489 let config = cargo_release_profile_config(&metadata).unwrap();
490 assert!(config.contains(STRIP_CONFIG));
491 assert!(!config.contains(LTO_CONFIG));
492 }
493}