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)?;
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)?;
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) -> Result<CargoMetadata, MetadataError> {
204 trace!("loading Cargo metadata");
205 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
206 metadata_cmd
207 .no_deps()
208 .verbose(enabled!(target: "cargo_lambda", Level::TRACE));
209
210 let manifest_ref = manifest_path.as_ref();
213
214 match (manifest_ref.parent(), manifest_ref.file_name()) {
215 (Some(project), Some(manifest)) if is_project_metadata_ok(project) => {
216 metadata_cmd.current_dir(project);
217 metadata_cmd.manifest_path(manifest);
218 }
219 _ => {
220 metadata_cmd.manifest_path(manifest_ref);
223 }
224 }
225
226 trace!(metadata = ?metadata_cmd, "loading cargo metadata");
227 let meta = metadata_cmd
228 .exec()
229 .map_err(MetadataError::FailedCmdExecution)?;
230 trace!(metadata = ?meta, "loaded cargo metadata");
231 Ok(meta)
232}
233
234pub fn main_binary_from_metadata(metadata: &CargoMetadata) -> Result<String, MetadataError> {
239 let targets = binary_targets_from_metadata(metadata, false);
240 if targets.len() > 1 {
241 let mut vec = targets.into_iter().collect::<Vec<_>>();
242 vec.sort();
243 Err(MetadataError::MultipleBinariesInProject(vec.join(", ")))
244 } else if targets.is_empty() {
245 Err(MetadataError::MissingBinaryInProject)
246 } else {
247 targets
248 .into_iter()
249 .next()
250 .ok_or(MetadataError::MissingBinaryInProject)
251 }
252}
253
254fn is_project_metadata_ok(path: &Path) -> bool {
255 path.is_dir() && metadata(path).is_ok()
256}
257
258pub(crate) fn serialize_common_options<S>(
259 state: &mut <S as serde::Serializer>::SerializeStruct,
260 opts: &CommonOptions,
261) -> Result<(), S::Error>
262where
263 S: serde::Serializer,
264{
265 if opts.quiet {
266 state.serialize_field("quiet", &true)?;
267 }
268 if let Some(jobs) = opts.jobs {
269 state.serialize_field("jobs", &jobs)?;
270 }
271 if opts.keep_going {
272 state.serialize_field("keep_going", &true)?;
273 }
274 if let Some(profile) = &opts.profile {
275 state.serialize_field("profile", profile)?;
276 }
277 if !opts.features.is_empty() {
278 state.serialize_field("features", &opts.features)?;
279 }
280 if opts.all_features {
281 state.serialize_field("all_features", &true)?;
282 }
283 if opts.no_default_features {
284 state.serialize_field("no_default_features", &true)?;
285 }
286 if !opts.target.is_empty() {
287 state.serialize_field("target", &opts.target)?;
288 }
289 if let Some(target_dir) = &opts.target_dir {
290 state.serialize_field("target_dir", target_dir)?;
291 }
292 if !opts.message_format.is_empty() {
293 state.serialize_field("message_format", &opts.message_format)?;
294 }
295 if opts.verbose > 0 {
296 state.serialize_field("verbose", &opts.verbose)?;
297 }
298 if let Some(color) = &opts.color {
299 state.serialize_field("color", color)?;
300 }
301 if opts.frozen {
302 state.serialize_field("frozen", &true)?;
303 }
304 if opts.locked {
305 state.serialize_field("locked", &true)?;
306 }
307 if opts.offline {
308 state.serialize_field("offline", &true)?;
309 }
310 if !opts.config.is_empty() {
311 state.serialize_field("config", &opts.config)?;
312 }
313 if !opts.unstable_flags.is_empty() {
314 state.serialize_field("unstable_flags", &opts.unstable_flags)?;
315 }
316 if let Some(timings) = &opts.timings {
317 state.serialize_field("timings", timings)?;
318 }
319
320 Ok(())
321}
322
323pub(crate) fn count_common_options(opts: &CommonOptions) -> usize {
324 opts.quiet as usize
325 + opts.jobs.is_some() as usize
326 + opts.keep_going as usize
327 + opts.profile.is_some() as usize
328 + !opts.features.is_empty() as usize
329 + opts.all_features as usize
330 + opts.no_default_features as usize
331 + !opts.target.is_empty() as usize
332 + opts.target_dir.is_some() as usize
333 + !opts.message_format.is_empty() as usize
334 + (opts.verbose > 0) as usize
335 + opts.color.is_some() as usize
336 + opts.frozen as usize
337 + opts.locked as usize
338 + opts.offline as usize
339 + !opts.config.is_empty() as usize
340 + !opts.unstable_flags.is_empty() as usize
341 + opts.timings.is_some() as usize
342}
343
344pub(crate) fn deserialize_vec_or_map<'de, D>(
345 deserializer: D,
346) -> Result<Option<Vec<String>>, D::Error>
347where
348 D: serde::Deserializer<'de>,
349{
350 let value = Value::deserialize(deserializer)?;
351
352 match value {
353 Value::Array(arr) => {
354 let el = arr
355 .into_iter()
356 .map(|v| v.as_str().map(String::from))
357 .collect::<Option<Vec<_>>>();
358 Ok(el)
359 }
360 Value::Object(map) => {
361 let el = map
362 .into_iter()
363 .map(|(k, v)| format!("{}={}", k, v.as_str().unwrap_or("")))
364 .collect();
365 Ok(Some(el))
366 }
367 _ => Ok(None),
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use crate::tests::fixture_metadata;
374
375 use super::*;
376
377 #[test]
378 fn test_binary_packages() {
379 let bins = binary_targets(fixture_metadata("single-binary-package"), false).unwrap();
380 assert_eq!(1, bins.len());
381 assert!(bins.contains("basic-lambda"));
382 }
383
384 #[test]
385 fn test_binary_packages_with_mutiple_bin_entries() {
386 let bins = binary_targets(fixture_metadata("multi-binary-package"), false).unwrap();
387 assert_eq!(5, bins.len());
388 assert!(bins.contains("delete-product"));
389 assert!(bins.contains("get-product"));
390 assert!(bins.contains("get-products"));
391 assert!(bins.contains("put-product"));
392 assert!(bins.contains("dynamodb-streams"));
393 }
394
395 #[test]
396 fn test_binary_packages_with_workspace() {
397 let bins = binary_targets(fixture_metadata("workspace-package"), false).unwrap();
398 assert_eq!(3, bins.len());
399 assert!(bins.contains("basic-lambda-1"));
400 assert!(bins.contains("basic-lambda-2"));
401 assert!(bins.contains("crate-3"));
402 }
403
404 #[test]
405 fn test_binary_packages_with_mixed_workspace() {
406 let bins = binary_targets(fixture_metadata("mixed-workspace-package"), false).unwrap();
407 assert_eq!(1, bins.len());
408 assert!(bins.contains("function-crate"), "{bins:?}");
409 }
410
411 #[test]
412 fn test_binary_packages_with_missing_binary_info() {
413 let err = binary_targets(fixture_metadata("missing-binary-package"), false).unwrap_err();
414 assert!(
415 err.to_string()
416 .contains("a [lib] section, or [[bin]] section must be present")
417 );
418 }
419
420 #[test]
421 fn test_main_binary_with_package_name() {
422 let manifest_path = fixture_metadata("single-binary-package");
423 let metadata = load_metadata(manifest_path).unwrap();
424 let name = main_binary_from_metadata(&metadata).unwrap();
425 assert_eq!("basic-lambda", name);
426 }
427
428 #[test]
429 fn test_main_binary_with_binary_name() {
430 let manifest_path = fixture_metadata("single-binary-different-name");
431 let metadata = load_metadata(manifest_path).unwrap();
432 let name = main_binary_from_metadata(&metadata).unwrap();
433 assert_eq!("basic-lambda-binary", name);
434 }
435
436 #[test]
437 fn test_main_binary_multi_binaries() {
438 let manifest_path = fixture_metadata("multi-binary-package");
439 let metadata = load_metadata(manifest_path).unwrap();
440 let err = main_binary_from_metadata(&metadata).unwrap_err();
441 assert_eq!(
442 "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",
443 err.to_string()
444 );
445 }
446
447 #[test]
448 fn test_select_binary() {
449 let manifest_path = fixture_metadata("multi-binary-package");
450 let metadata = load_metadata(manifest_path).unwrap();
451
452 let package_filter: Option<fn(&&CargoPackage) -> bool> = None;
453
454 let bin = "delete-product".to_string();
455 let binary_filter = selected_bin_filter(vec![bin.clone()]);
456
457 let binaries =
458 filter_binary_targets_from_metadata(&metadata, binary_filter, package_filter);
459
460 assert_eq!(1, binaries.len());
461 assert!(binaries.contains(&bin));
462 }
463
464 #[test]
465 fn test_example_packages() {
466 let bins = binary_targets(fixture_metadata("examples-package"), true).unwrap();
467 assert_eq!(1, bins.len());
468 assert!(bins.contains("example-lambda"));
469 }
470
471 #[test]
472 fn test_release_config() {
473 let config = cargo_release_profile_config_from_metadata(Metadata::default());
474 assert!(config.contains(STRIP_CONFIG));
475 assert!(config.contains(LTO_CONFIG));
476 assert!(config.contains(CODEGEN_CONFIG));
477 assert!(config.contains(PANIC_CONFIG));
478 }
479
480 #[test]
481 fn test_release_config_with_workspace() {
482 let metadata = load_metadata(fixture_metadata("workspace-package")).unwrap();
483 let config = cargo_release_profile_config(&metadata).unwrap();
484 assert!(config.contains(STRIP_CONFIG));
485 assert!(!config.contains(LTO_CONFIG));
486 }
487}