Skip to main content

cargo_lambda_metadata/cargo/
mod.rs

1pub 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
65/// Extract all the binary target names from a Cargo.toml file
66pub 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
97// Several targets can have `crate_type` be `bin`, we're only
98// interested in the ones which `kind` is `bin` or `example`.
99// See https://doc.rust-lang.org/cargo/commands/cargo-metadata.html?highlight=targets%20metadata#json-format
100pub 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
104/// Extract all the binary target names from a Cargo.toml file
105pub 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
149/// Extract target directory information
150///
151/// This fetches the target directory from `cargo metadata`, resolving the
152/// user and project configuration and the environment variables in the right
153/// way.
154pub fn target_dir_from_metadata(metadata: &CargoMetadata) -> Result<PathBuf> {
155    Ok(metadata.target_directory.clone().into_std_path_buf())
156}
157
158/// Attempt to read the release profile section in the Cargo manifest.
159/// Cargo metadata doesn't expose profile information, so we try
160/// to read it from the Cargo.toml file directly.
161pub 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/// Create metadata about the root package in the Cargo manifest, without any dependencies.
200#[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 target-dir was explicitly specified, pass it to cargo metadata
212    if let Some(dir) = target_dir {
213        metadata_cmd.other_options(vec![format!("--target-dir={}", dir.display())]);
214    }
215
216    // try to split manifest path and assign current_dir to enable parsing a project-specific
217    // cargo config
218    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            // fall back to using the manifest_path without changing the dir
227            // this means there will not be any project-specific config parsing
228            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
240/// Load the main binary in the project.
241/// It returns an error if the project includes from than one binary.
242/// Use this function when the user didn't provide any funcion name
243/// assuming that there is only one binary in the project
244pub 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}