diff --git a/Cargo.lock b/Cargo.lock index fcc0578..c9da4f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,9 +517,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -570,9 +570,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -690,18 +690,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -843,9 +843,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] diff --git a/README.md b/README.md index 4a52834..8a13cf9 100644 --- a/README.md +++ b/README.md @@ -104,4 +104,4 @@ Do not rely on SBOMs when dealing with supply chain attacks! ### What is blocking uplifting this into Cargo? -The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553) is implemented. +The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553) is implemented. That RFC has now been implemented and is available via an [unstable feature](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom). cargo-auditable integrates with this: if you enable that feature and build with cargo auditable, e.g with `CARGO_BUILD_SBOM=true cargo auditable -Z sbom build` and a nightly Rust toolchain, then cargo auditable will use the SBOM precursor files generated by cargo. diff --git a/cargo-auditable/src/collect_audit_data.rs b/cargo-auditable/src/collect_audit_data.rs index 5f20046..ffbad46 100644 --- a/cargo-auditable/src/collect_audit_data.rs +++ b/cargo-auditable/src/collect_audit_data.rs @@ -4,13 +4,30 @@ use std::str::from_utf8; use crate::{ auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs, - rustc_arguments::RustcArgs, + rustc_arguments::RustcArgs, sbom_precursor, }; /// Calls `cargo metadata` to obtain the dependency tree, serializes it to JSON and compresses it pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) -> Vec { - let metadata = get_metadata(rustc_args, target_triple); - let version_info = encode_audit_data(&metadata).unwrap(); + let sbom_path = std::env::var_os("CARGO_SBOM_PATH"); + + // If cargo has created precursor SBOM files, use them instead of `cargo metadata`. + let version_info = if sbom_path.as_ref().map(|p| !p.is_empty()).unwrap_or(false) { + // Cargo creates an SBOM file for each output file (rlib, bin, cdylib, etc), + // but the SBOM file is identical for each output file in a given rustc crate compilation, + // so we can just use the first SBOM we find. + let sbom_path = std::env::split_paths(&sbom_path.unwrap()).next().unwrap(); + let sbom_data: Vec = std::fs::read(&sbom_path) + .unwrap_or_else(|_| panic!("Failed to read SBOM file at {}", sbom_path.display())); + let sbom_precursor: sbom_precursor::SbomPrecursor = serde_json::from_slice(&sbom_data) + .unwrap_or_else(|_| panic!("Failed to parse SBOM file at {}", sbom_path.display())); + sbom_precursor.into() + } else { + // If no SBOM files are available, fall back to `cargo metadata` + let metadata = get_metadata(rustc_args, target_triple); + encode_audit_data(&metadata).unwrap() + }; + let json = serde_json::to_string(&version_info).unwrap(); // compression level 7 makes this complete in a few milliseconds, so no need to drop to a lower level in debug mode let compressed_json = compress_to_vec_zlib(json.as_bytes(), 7); diff --git a/cargo-auditable/src/main.rs b/cargo-auditable/src/main.rs index ad12a51..f4d88b7 100644 --- a/cargo-auditable/src/main.rs +++ b/cargo-auditable/src/main.rs @@ -9,6 +9,7 @@ mod object_file; mod platform_detection; mod rustc_arguments; mod rustc_wrapper; +mod sbom_precursor; mod target_info; use std::process::exit; diff --git a/cargo-auditable/src/sbom_precursor.rs b/cargo-auditable/src/sbom_precursor.rs new file mode 100644 index 0000000..79a22a2 --- /dev/null +++ b/cargo-auditable/src/sbom_precursor.rs @@ -0,0 +1,199 @@ +use std::collections::HashMap; + +use auditable_serde::{Package, Source, VersionInfo}; +use cargo_metadata::{ + semver::{self, Version}, + DependencyKind, +}; +use serde::{Deserialize, Serialize}; + +/// Cargo SBOM precursor format. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SbomPrecursor { + /// Schema version + pub version: u32, + /// Index into the crates array for the root crate + pub root: usize, + /// Array of all crates + pub crates: Vec, + /// Information about rustc used to perform the compilation + pub rustc: RustcInfo, +} + +impl From for VersionInfo { + fn from(sbom: SbomPrecursor) -> Self { + // cargo sbom data format has more nodes than the auditable info format - if a crate is both a build + // and runtime dependency it will appear twice in the `crates` array. + // The `VersionInfo` format lists each package only once, with a single `kind` field + // (Runtime having precedence over other kinds). + + // Firstly, we deduplicate the (name, version) pairs and create a mapping from the + // original indices in the cargo sbom array to the new index in the auditable info package array. + let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold( + (HashMap::new(), Vec::new(), Vec::new()), + |(mut id_to_index_map, mut packages, mut indices), (index, crate_)| { + match id_to_index_map.entry(crate_.id.clone()) { + std::collections::hash_map::Entry::Occupied(entry) => { + // Just store the new index in the indices array + indices.push(*entry.get()); + } + std::collections::hash_map::Entry::Vacant(entry) => { + let (name, version, source) = parse_fully_qualified_package_id(&crate_.id); + // If the entry does not exist, we create it + packages.push(Package { + name, + version, + source, + // Assume build, if we determine this is a runtime dependency we'll update later + kind: auditable_serde::DependencyKind::Build, + // We will fill this in later + dependencies: Vec::new(), + root: index == sbom.root, + }); + entry.insert(packages.len() - 1); + indices.push(packages.len() - 1); + } + } + (id_to_index_map, packages, indices) + }, + ); + + // Traverse the graph as given by the sbom to fill in the dependencies with the new indices. + // + // Keep track of whether the dependency is a runtime dependency. + // If we ever encounter a non-runtime dependency, all deps in the remaining subtree + // are not runtime dependencies, i.e a runtime dep of a build dep is not recognized as a runtime dep. + let mut stack = Vec::new(); + stack.push((sbom.root, true)); + while let Some((old_index, is_runtime)) = stack.pop() { + let crate_ = &sbom.crates[old_index]; + for dep in &crate_.dependencies { + stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime)); + } + + let package = &mut packages[indices[old_index]]; + if is_runtime { + package.kind = auditable_serde::DependencyKind::Runtime + }; + + for dep in &crate_.dependencies { + let new_dep_index = indices[dep.index]; + if package.dependencies.contains(&new_dep_index) { + continue; // Already added this dependency + } else if new_dep_index == indices[old_index] { + // If the dependency is the same as the package itself, skip it + continue; + } else { + package.dependencies.push(new_dep_index); + } + } + } + + VersionInfo { packages } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Crate { + /// Package ID specification + pub id: String, + /// List of target kinds + pub kind: Vec, + /// Enabled feature flags + pub features: Vec, + /// Dependencies for this crate + pub dependencies: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Dependency { + /// Index into the crates array + pub index: usize, + /// Dependency kind: "normal", "build", or "dev" + pub kind: DependencyKind, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RustcInfo { + /// Compiler version + pub version: String, + /// Compiler wrapper + pub wrapper: Option, + /// Compiler workspace wrapper + pub workspace_wrapper: Option, + /// Commit hash for rustc + pub commit_hash: String, + /// Host target triple + pub host: String, + /// Verbose version string: `rustc -vV` + pub verbose_version: String, +} + +const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index"; + +/// Parses a fully qualified package ID spec string into a tuple of (name, version, source). +/// The package ID spec format is defined at https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#package-id-specifications-1 +/// +/// The fully qualified form of a package ID spec is mentioned in the Cargo documentation, +/// figuring it out is left as an exercise to the reader. +/// +/// Adapting the grammar in the cargo doc, the format appears to be : +/// ```norust +/// fully_qualified_spec := kind "+" proto "://" hostname-and-path [ "?" query] "#" [ name "@" ] semver +/// query := ( "branch" | "tag" | "rev" ) "=" ref +/// semver := digits "." digits "." digits [ "-" prerelease ] [ "+" build ] +/// kind := "registry" | "git" | "path" +/// proto := "http" | "git" | "file" | ... +/// ``` +/// where: +/// - the name is always present except when the kind is `path` and the last segment of the path doesn't match the name +/// - the query string is only present for git dependencies (which we can ignore since we don't record git information) +fn parse_fully_qualified_package_id(id: &str) -> (String, Version, Source) { + let (kind, rest) = id.split_once('+').expect("Package ID to have a kind"); + let (url, rest) = rest + .split_once('#') + .expect("Package ID to have version information"); + let source = match (kind, url) { + ("registry", CRATES_IO_INDEX) => Source::CratesIo, + ("registry", _) => Source::Registry, + ("git", _) => Source::Git, + ("path", _) => Source::Local, + _ => Source::Other(kind.to_string()), + }; + + if source == Source::Local { + // For local packages, the name might be in the suffix after '#' if it has + // a diferent name than the last segment of the path. + if let Some((name, version)) = rest.split_once('@') { + ( + name.to_string(), + semver::Version::parse(version).expect("Version to be valid SemVer"), + source, + ) + } else { + // If no name is specified, use the last segment of the path as the name + let name = url + .split('/') + .next_back() + .unwrap() + .split('\\') + .next_back() + .unwrap(); + ( + name.to_string(), + semver::Version::parse(rest).expect("Version to be valid SemVer"), + source, + ) + } + } else { + // For other sources, the name and version are after the '#', separated by '@' + let (name, version) = rest + .split_once('@') + .expect("Package ID to have a name and version"); + ( + name.to_string(), + semver::Version::parse(version).expect("Version to be valid SemVer"), + source, + ) + } +} diff --git a/cargo-auditable/tests/.gitignore b/cargo-auditable/tests/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/cargo-auditable/tests/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs b/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/build_then_runtime_dep/top_level_crate/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs b/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/custom_rustc_path/runtime_dep/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml new file mode 100644 index 0000000..c22d0be --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "bar" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/bar/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml new file mode 100644 index 0000000..35150f4 --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "foo" +version = "0.1.0" +edition = "2021" + +[dependencies] +bar = { version = "0.1.0", path = "../bar" } +baz = { version = "0.1.0", path = "../qux" } + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/foo/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml new file mode 100644 index 0000000..f379c9f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "baz" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[workspace] diff --git a/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/cargo-auditable/tests/fixtures/path_not_equal_name/qux/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs b/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/cargo-auditable/tests/fixtures/runtime_then_build_dep/runtime_dep/build.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/cargo-auditable/tests/it.rs b/cargo-auditable/tests/it.rs index ec94ac8..9e3377a 100644 --- a/cargo-auditable/tests/it.rs +++ b/cargo-auditable/tests/it.rs @@ -22,10 +22,12 @@ const CARGO: &str = env!("CARGO"); /// Run cargo auditable with --manifest-path and extra args, /// returning of map of workspace member names -> produced binaries (bin and cdylib) /// Reads the AUDITABLE_TEST_TARGET environment variable to determine the target to compile for +/// Uses `CARGO_BUILD_SBOM` environment variable to enable SBOM generation if `sbom` is true fn run_cargo_auditable

( cargo_toml_path: P, args: &[&str], env: &[(&str, &OsStr)], + sbom: bool, ) -> HashMap> where P: AsRef, @@ -49,8 +51,16 @@ where .arg("--manifest-path") .arg(&cargo_toml_path) // We'll parse these to get binary paths - .arg("--message-format=json") - .args(args); + .arg("--message-format=json"); + + if sbom { + command.arg("-Z").arg("sbom"); + command.env("CARGO_BUILD_SBOM", "true"); + // Enable SBOM tests to run on stable rust + command.env("RUSTC_BOOTSTRAP", "1"); + } + + command.args(args); if let Ok(target) = std::env::var("AUDITABLE_TEST_TARGET") { if args.iter().all(|arg| !arg.starts_with("--target")) { @@ -153,11 +163,16 @@ fn get_dependency_info(binary: &Utf8Path) -> VersionInfo { #[test] fn test_cargo_auditable_workspaces() { + test_cargo_auditable_workspaces_inner(false); + test_cargo_auditable_workspaces_inner(true); +} + +fn test_cargo_auditable_workspaces_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/workspace/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(&workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // No binaries for library_crate assert!(bins.get("library_crate").is_none()); @@ -199,6 +214,7 @@ fn test_cargo_auditable_workspaces() { &workspace_cargo_toml, &["--features", "binary_and_cdylib_crate"], &[], + sbom, ); // crate_with_features should now have three dependencies, library_crate binary_and_cdylib_crate and crate_with_features, let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; @@ -216,7 +232,7 @@ fn test_cargo_auditable_workspaces() { .any(|p| p.name == "binary_and_cdylib_crate")); // Run without default features - let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-default-features"], &[]); + let bins = run_cargo_auditable(&workspace_cargo_toml, &["--no-default-features"], &[], sbom); // crate_with_features should now only depend on itself let crate_with_features_bin = &bins.get("crate_with_features").unwrap()[0]; let dep_info = get_dependency_info(crate_with_features_bin); @@ -231,11 +247,16 @@ fn test_cargo_auditable_workspaces() { /// This exercises a small real-world project #[test] fn test_self_hosting() { + test_self_hosting_inner(false); + test_self_hosting_inner(true); +} + +fn test_self_hosting_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../rust-audit-info/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Self-hosting binary map: {bins:?}"); // verify that the dependency info is present at all @@ -251,11 +272,15 @@ fn test_self_hosting() { #[test] fn test_lto() { + test_lto_inner(false); + test_lto_inner(true); +} +fn test_lto_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lto_binary_crate/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("LTO binary map: {bins:?}"); // lto_binary_crate should only depend on itself @@ -271,11 +296,16 @@ fn test_lto() { #[test] fn test_lto_stripped() { + test_lto_stripped_inner(false); + test_lto_stripped_inner(true); +} + +fn test_lto_stripped_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lto_stripped_binary/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Stripped binary map: {bins:?}"); // lto_stripped_binary should only depend on itself @@ -291,11 +321,15 @@ fn test_lto_stripped() { #[test] fn test_bin_and_lib_in_one_crate() { + test_bin_and_lib_in_one_crate_inner(false); + test_bin_and_lib_in_one_crate_inner(true); +} +fn test_bin_and_lib_in_one_crate_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/lib_and_bin_crate/Cargo.toml"); - let bins = run_cargo_auditable(workspace_cargo_toml, &["--bin=some_binary"], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &["--bin=some_binary"], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // lib_and_bin_crate should only depend on itself @@ -309,15 +343,17 @@ fn test_bin_and_lib_in_one_crate() { .any(|p| p.name == "lib_and_bin_crate")); } -/// A previous approach had trouble with build scripts and proc macros. -/// Verify that those still work. #[test] fn test_build_script() { + test_build_script_inner(false); + test_build_script_inner(true); +} +fn test_build_script_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/crate_with_build_script/Cargo.toml"); - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // crate_with_build_script should only depend on itself @@ -333,11 +369,15 @@ fn test_build_script() { #[test] fn test_platform_specific_deps() { + test_platform_specific_deps_inner(false); + test_platform_specific_deps_inner(true); +} +fn test_platform_specific_deps_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/platform_specific_deps/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); let test_target = std::env::var("AUDITABLE_TEST_TARGET"); @@ -357,11 +397,15 @@ fn test_platform_specific_deps() { #[test] fn test_build_then_runtime_dep() { + test_build_then_runtime_dep_inner(false); + test_build_then_runtime_dep_inner(true); +} +fn test_build_then_runtime_dep_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/build_then_runtime_dep/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly @@ -381,11 +425,15 @@ fn test_build_then_runtime_dep() { #[test] fn test_runtime_then_build_dep() { + test_runtime_then_build_dep_inner(false); + test_runtime_then_build_dep_inner(true); +} +fn test_runtime_then_build_dep_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/runtime_then_build_dep/Cargo.toml"); // Run in workspace root with default features - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[]); + let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[], sbom); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly @@ -405,13 +453,22 @@ fn test_runtime_then_build_dep() { #[test] fn test_custom_rustc_path() { + test_custom_rustc_path_inner(false); + test_custom_rustc_path_inner(true); +} +fn test_custom_rustc_path_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/custom_rustc_path/Cargo.toml"); // locate rustc let rustc_path = which::which("rustc").unwrap(); // Run in workspace root with a custom path to rustc - let bins = run_cargo_auditable(workspace_cargo_toml, &[], &[("RUSTC", rustc_path.as_ref())]); + let bins = run_cargo_auditable( + workspace_cargo_toml, + &[], + &[("RUSTC", rustc_path.as_ref())], + sbom, + ); eprintln!("Test fixture binary map: {bins:?}"); // check that the build types are propagated correctly @@ -447,6 +504,11 @@ fn test_workspace_member_version_info() { #[test] fn test_wasm() { + test_wasm_inner(false); + test_wasm_inner(true); +} + +fn test_wasm_inner(sbom: bool) { // Path to workspace fixture Cargo.toml. See that file for overview of workspace members and their dependencies. let workspace_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wasm_crate/Cargo.toml"); @@ -455,6 +517,7 @@ fn test_wasm() { workspace_cargo_toml, &["--target=wasm32-unknown-unknown"], &[], + sbom, ); // check that the build types are propagated correctly @@ -464,3 +527,28 @@ fn test_wasm() { eprintln!("wasm_crate.wasm dependency info: {dep_info:?}"); assert_eq!(dep_info.packages.len(), 16); } + +#[test] +fn test_path_not_equal_name() { + test_path_not_equal_name_inner(false); + test_path_not_equal_name_inner(true); +} + +fn test_path_not_equal_name_inner(sbom: bool) { + // This tests a case where a path dependency's directory name is not equal to the crate name. + let cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/path_not_equal_name/foo/Cargo.toml"); + let bins = run_cargo_auditable(cargo_toml, &[], &[], sbom); + let foo_bin = &bins.get("foo").unwrap()[0]; + let dep_info = get_dependency_info(foo_bin); + eprintln!("{foo_bin} dependency info: {dep_info:?}"); + assert!(dep_info.packages.len() == 3); + assert!(dep_info + .packages + .iter() + .any(|p| p.name == "bar" && p.kind == DependencyKind::Runtime)); + assert!(dep_info + .packages + .iter() + .any(|p| p.name == "baz" && p.kind == DependencyKind::Runtime)); +}