From 69453aad3c0633e345cfad76c93e938caf2c7dfb Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 26 Feb 2024 11:24:55 +0100 Subject: [PATCH 01/22] Explore location to generate SBOM precursor files Similar to the generation of `depinfo` files, a function is called to generated SBOM precursor file named `output_sbom`. It takes the `BuildRunner` & the current `Unit`. The `sbom` flag can be specified as a cargo build option, but it's currently not configured fully. To test the generation the flag is set to `true`. * use SBOM types to serialize data --- src/cargo/core/compiler/build_config.rs | 8 ++ src/cargo/core/compiler/build_runner/mod.rs | 4 + src/cargo/core/compiler/mod.rs | 2 + src/cargo/core/compiler/output_sbom.rs | 100 ++++++++++++++++++++ src/cargo/util/context/mod.rs | 1 + 5 files changed, 115 insertions(+) create mode 100644 src/cargo/core/compiler/output_sbom.rs diff --git a/src/cargo/core/compiler/build_config.rs b/src/cargo/core/compiler/build_config.rs index 0f66d6dbbc6..a397d2a924f 100644 --- a/src/cargo/core/compiler/build_config.rs +++ b/src/cargo/core/compiler/build_config.rs @@ -46,6 +46,8 @@ pub struct BuildConfig { pub future_incompat_report: bool, /// Which kinds of build timings to output (empty if none). pub timing_outputs: Vec, + /// Output SBOM precursor files. + pub sbom: bool, } fn default_parallelism() -> CargoResult { @@ -102,6 +104,11 @@ impl BuildConfig { anyhow::bail!("-Zbuild-std requires --target"); } + let sbom = match gctx.get_env_os("CARGO_BUILD_SBOM") { + Some(sbom) => sbom == "true", + None => cfg.sbom == Some(true), + }; + Ok(BuildConfig { requested_kinds, jobs, @@ -117,6 +124,7 @@ impl BuildConfig { export_dir: None, future_incompat_report: false, timing_outputs: Vec::new(), + sbom, }) } diff --git a/src/cargo/core/compiler/build_runner/mod.rs b/src/cargo/core/compiler/build_runner/mod.rs index f9e3cf64c3d..cc50789dce3 100644 --- a/src/cargo/core/compiler/build_runner/mod.rs +++ b/src/cargo/core/compiler/build_runner/mod.rs @@ -291,6 +291,10 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { } super::output_depinfo(&mut self, unit)?; + + if self.bcx.build_config.sbom { + super::output_sbom(&mut self, unit)?; + } } for (script_meta, output) in self.build_script_outputs.lock().unwrap().iter() { diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index fe07c59945d..1372e97c839 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -47,6 +47,7 @@ pub(crate) mod layout; mod links; mod lto; mod output_depinfo; +mod output_sbom; pub mod rustdoc; pub mod standard_lib; mod timings; @@ -84,6 +85,7 @@ use self::job_queue::{Job, JobQueue, JobState, Work}; pub(crate) use self::layout::Layout; pub use self::lto::Lto; use self::output_depinfo::output_depinfo; +use self::output_sbom::output_sbom; use self::unit_graph::UnitDep; use crate::core::compiler::future_incompat::FutureIncompatReport; pub use crate::core::compiler::unit::{Unit, UnitInterner}; diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs new file mode 100644 index 00000000000..7210723bfa9 --- /dev/null +++ b/src/cargo/core/compiler/output_sbom.rs @@ -0,0 +1,100 @@ +//! cargo-sbom precursor files for external tools to create SBOM files from. +//! See [`output_sbom`] for more. + +use std::io::{BufWriter, Write}; + +use cargo_util::paths::{self}; +use cargo_util_schemas::core::PackageIdSpec; +use serde::Serialize; + +use crate::core::{Target, TargetKind}; +use crate::{core::compiler::FileFlavor, CargoResult}; + +use super::{BuildRunner, CrateType, Unit}; + +/// Typed version of a SBOM format version number. +pub struct SbomFormatVersion; + +impl Serialize for SbomFormatVersion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u32(V) + } +} + +#[derive(Serialize)] +struct SbomTarget { + kind: TargetKind, + crate_type: Option, + name: String, + edition: String, +} + +impl From<&Target> for SbomTarget { + fn from(target: &Target) -> Self { + SbomTarget { + kind: target.kind().clone(), + crate_type: target.kind().rustc_crate_types().first().cloned(), + name: target.name().to_string(), + edition: target.edition().to_string(), + } + } +} + +#[derive(Serialize)] +struct Sbom { + format_version: SbomFormatVersion<1>, + package_id: PackageIdSpec, + name: String, + version: String, + target: SbomTarget, + dependencies: Vec, + features: Vec, +} + +impl Sbom { + pub fn new(unit: &Unit) -> Self { + let package_id = unit.pkg.summary().package_id().to_spec(); + let name = unit.pkg.name().to_string(); + let version = unit.pkg.version().to_string(); + let features = unit.features.iter().map(|f| f.to_string()).collect(); + let target: SbomTarget = (&unit.target).into(); + + Self { + format_version: SbomFormatVersion, + package_id, + name, + version, + target, + dependencies: Vec::new(), + features, + } + } +} + +/// Saves a `.cargo-sbom.json` file for the given [`Unit`]. +/// +pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> { + let _bcx = build_runner.bcx; + + // TODO collect build & unit data, then transform into JSON output + for output in build_runner + .outputs(unit)? + .iter() + .filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable)) + { + if let Some(ref link_dst) = output.hardlink { + let output_path = link_dst.with_extension("cargo-sbom.json"); + + let sbom = Sbom::new(unit); + + let mut outfile = BufWriter::new(paths::create(output_path)?); + let output = serde_json::to_string_pretty(&sbom)?; + write!(outfile, "{}", output)?; + } + } + + Ok(()) +} diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index a8fee94d5e4..23084d086ef 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2615,6 +2615,7 @@ pub struct CargoBuildConfig { // deprecated alias for artifact-dir pub out_dir: Option, pub artifact_dir: Option, + pub sbom: Option, } /// Configuration for `build.target`. From 6253b310e2cf49e94b0742e35077efbac155c7d7 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 26 Feb 2024 12:31:39 +0100 Subject: [PATCH 02/22] Output source, profile & dependencies --- src/cargo/core/compiler/output_sbom.rs | 74 +++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs index 7210723bfa9..1f3c9278a87 100644 --- a/src/cargo/core/compiler/output_sbom.rs +++ b/src/cargo/core/compiler/output_sbom.rs @@ -2,15 +2,19 @@ //! See [`output_sbom`] for more. use std::io::{BufWriter, Write}; +use std::path::PathBuf; use cargo_util::paths::{self}; use cargo_util_schemas::core::PackageIdSpec; +use itertools::Itertools; use serde::Serialize; +use crate::core::profiles::Profile; use crate::core::{Target, TargetKind}; +use crate::util::Rustc; use crate::{core::compiler::FileFlavor, CargoResult}; -use super::{BuildRunner, CrateType, Unit}; +use super::{unit_graph::UnitDep, BuildRunner, CrateType, Unit}; /// Typed version of a SBOM format version number. pub struct SbomFormatVersion; @@ -24,6 +28,30 @@ impl Serialize for SbomFormatVersion { } } +#[derive(Serialize, Clone, Debug)] +struct SbomDependency { + package_id: PackageIdSpec, + features: Vec, + extern_crate_name: String, +} + +impl From<&UnitDep> for SbomDependency { + fn from(dep: &UnitDep) -> Self { + let features = dep + .unit + .features + .iter() + .map(|dep| dep.to_string()) + .collect_vec(); + + Self { + package_id: dep.unit.pkg.package_id().to_spec(), + features, + extern_crate_name: dep.extern_crate_name.to_string(), + } + } +} + #[derive(Serialize)] struct SbomTarget { kind: TargetKind, @@ -43,33 +71,60 @@ impl From<&Target> for SbomTarget { } } +#[derive(Serialize)] +struct SbomRustc { + version: String, + wrapper: Option, + commit_hash: Option, + host: String, +} + +impl From<&Rustc> for SbomRustc { + fn from(rustc: &Rustc) -> Self { + Self { + version: rustc.version.to_string(), + wrapper: rustc.wrapper.clone(), + commit_hash: rustc.commit_hash.clone(), + host: rustc.host.to_string(), + } + } +} + #[derive(Serialize)] struct Sbom { format_version: SbomFormatVersion<1>, package_id: PackageIdSpec, name: String, version: String, + source: String, target: SbomTarget, - dependencies: Vec, + profile: Profile, + dependencies: Vec, features: Vec, + rustc: SbomRustc, } impl Sbom { - pub fn new(unit: &Unit) -> Self { + pub fn new(unit: &Unit, dependencies: Vec, rustc: SbomRustc) -> Self { let package_id = unit.pkg.summary().package_id().to_spec(); let name = unit.pkg.name().to_string(); let version = unit.pkg.version().to_string(); + let source = unit.pkg.package_id().source_id().to_string(); + let target = (&unit.target).into(); + let profile = unit.profile.clone(); let features = unit.features.iter().map(|f| f.to_string()).collect(); - let target: SbomTarget = (&unit.target).into(); Self { format_version: SbomFormatVersion, package_id, name, version, + source, target, - dependencies: Vec::new(), + profile, + dependencies, features, + rustc, } } } @@ -77,7 +132,9 @@ impl Sbom { /// Saves a `.cargo-sbom.json` file for the given [`Unit`]. /// pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> { - let _bcx = build_runner.bcx; + let bcx = build_runner.bcx; + let unit_deps = build_runner.unit_deps(unit); + let dependencies = unit_deps.iter().map(|dep| dep.into()).collect_vec(); // TODO collect build & unit data, then transform into JSON output for output in build_runner @@ -88,9 +145,10 @@ pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Cargo if let Some(ref link_dst) = output.hardlink { let output_path = link_dst.with_extension("cargo-sbom.json"); - let sbom = Sbom::new(unit); + let rustc = bcx.rustc().into(); + let sbom = Sbom::new(unit, dependencies.clone(), rustc); - let mut outfile = BufWriter::new(paths::create(output_path)?); + let mut outfile = BufWriter::new(paths::create(output_path.clone())?); let output = serde_json::to_string_pretty(&sbom)?; write!(outfile, "{}", output)?; } From 23f087e01d0b22e65899811f6a11cbf25e388f47 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 26 Feb 2024 16:29:19 +0100 Subject: [PATCH 03/22] Trying to fetch all dependencies This ignores dependencies for custom build scripts. The output should be similar to what `cargo tree` reports. --- src/cargo/core/compiler/output_sbom.rs | 38 ++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs index 1f3c9278a87..829e5eeb963 100644 --- a/src/cargo/core/compiler/output_sbom.rs +++ b/src/cargo/core/compiler/output_sbom.rs @@ -1,6 +1,7 @@ //! cargo-sbom precursor files for external tools to create SBOM files from. //! See [`output_sbom`] for more. +use std::collections::BTreeSet; use std::io::{BufWriter, Write}; use std::path::PathBuf; @@ -31,6 +32,8 @@ impl Serialize for SbomFormatVersion { #[derive(Serialize, Clone, Debug)] struct SbomDependency { package_id: PackageIdSpec, + package: String, + version: String, features: Vec, extern_crate_name: String, } @@ -46,6 +49,8 @@ impl From<&UnitDep> for SbomDependency { Self { package_id: dep.unit.pkg.package_id().to_spec(), + package: dep.unit.pkg.package_id().name().to_string(), + version: dep.unit.pkg.package_id().version().to_string(), features, extern_crate_name: dep.extern_crate_name.to_string(), } @@ -133,8 +138,8 @@ impl Sbom { /// pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> { let bcx = build_runner.bcx; - let unit_deps = build_runner.unit_deps(unit); - let dependencies = unit_deps.iter().map(|dep| dep.into()).collect_vec(); + + let dependencies = fetch_dependencies(build_runner, unit); // TODO collect build & unit data, then transform into JSON output for output in build_runner @@ -156,3 +161,32 @@ pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Cargo Ok(()) } + +/// Fetch all dependencies, including transitive ones. A dependency can also appear multiple times +/// if it's included with different versions. +fn fetch_dependencies(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Vec { + let unit_graph = &build_runner.bcx.unit_graph; + let root_deps = build_runner.unit_deps(unit); + + let mut result = Vec::new(); + let mut queue: BTreeSet<&UnitDep> = root_deps.iter().collect(); + let mut visited: BTreeSet<&UnitDep> = BTreeSet::new(); + + while let Some(dependency) = queue.pop_first() { + // ignore any custom build scripts. + if dependency.unit.mode.is_run_custom_build() { + continue; + } + if visited.contains(dependency) { + continue; + } + + result.push(dependency); + visited.insert(dependency); + + let mut dependencies: BTreeSet<&UnitDep> = unit_graph[&dependency.unit].iter().collect(); + queue.append(&mut dependencies); + } + + result.into_iter().map(|d| d.into()).collect_vec() +} From 2b11115e37b1643f331dfd3135a3d7a2e270db8b Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 27 Feb 2024 11:53:37 +0100 Subject: [PATCH 04/22] Output package dependencies This is similar to what the `cargo metadata` command outputs. --- src/cargo/core/compiler/output_sbom.rs | 86 +++++++++++++++++++------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs index 829e5eeb963..38b63038b21 100644 --- a/src/cargo/core/compiler/output_sbom.rs +++ b/src/cargo/core/compiler/output_sbom.rs @@ -10,7 +10,7 @@ use cargo_util_schemas::core::PackageIdSpec; use itertools::Itertools; use serde::Serialize; -use crate::core::profiles::Profile; +use crate::core::{profiles::Profile, PackageId}; use crate::core::{Target, TargetKind}; use crate::util::Rustc; use crate::{core::compiler::FileFlavor, CargoResult}; @@ -29,30 +29,65 @@ impl Serialize for SbomFormatVersion { } } +/// A package dependency #[derive(Serialize, Clone, Debug)] struct SbomDependency { - package_id: PackageIdSpec, - package: String, + name: String, + package_id: PackageId, version: String, features: Vec, - extern_crate_name: String, } impl From<&UnitDep> for SbomDependency { fn from(dep: &UnitDep) -> Self { + let package_id = dep.unit.pkg.package_id(); + let name = package_id.name().to_string(); + let version = package_id.version().to_string(); let features = dep .unit .features .iter() - .map(|dep| dep.to_string()) + .map(|f| f.to_string()) .collect_vec(); Self { - package_id: dep.unit.pkg.package_id().to_spec(), - package: dep.unit.pkg.package_id().name().to_string(), - version: dep.unit.pkg.package_id().version().to_string(), + name, + package_id, + version, + features, + } + } +} + +#[derive(Serialize, Clone, Debug)] +struct SbomPackage { + package_id: PackageId, + package: String, + version: String, + features: Vec, + extern_crate_name: String, + dependencies: Vec, +} + +impl SbomPackage { + pub fn new(dep: &UnitDep, dependencies: Vec) -> Self { + let package_id = dep.unit.pkg.package_id(); + let package = package_id.name().to_string(); + let version = package_id.version().to_string(); + let features = dep + .unit + .features + .iter() + .map(|f| f.to_string()) + .collect_vec(); + + Self { + package_id, + package, + version, features, extern_crate_name: dep.extern_crate_name.to_string(), + dependencies, } } } @@ -76,7 +111,7 @@ impl From<&Target> for SbomTarget { } } -#[derive(Serialize)] +#[derive(Serialize, Clone)] struct SbomRustc { version: String, wrapper: Option, @@ -104,13 +139,13 @@ struct Sbom { source: String, target: SbomTarget, profile: Profile, - dependencies: Vec, + packages: Vec, features: Vec, rustc: SbomRustc, } impl Sbom { - pub fn new(unit: &Unit, dependencies: Vec, rustc: SbomRustc) -> Self { + pub fn new(unit: &Unit, packages: Vec, rustc: SbomRustc) -> Self { let package_id = unit.pkg.summary().package_id().to_spec(); let name = unit.pkg.name().to_string(); let version = unit.pkg.version().to_string(); @@ -127,7 +162,7 @@ impl Sbom { source, target, profile, - dependencies, + packages, features, rustc, } @@ -138,8 +173,9 @@ impl Sbom { /// pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<()> { let bcx = build_runner.bcx; + let rustc: SbomRustc = bcx.rustc().into(); - let dependencies = fetch_dependencies(build_runner, unit); + let packages = fetch_packages(build_runner, unit); // TODO collect build & unit data, then transform into JSON output for output in build_runner @@ -150,8 +186,7 @@ pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Cargo if let Some(ref link_dst) = output.hardlink { let output_path = link_dst.with_extension("cargo-sbom.json"); - let rustc = bcx.rustc().into(); - let sbom = Sbom::new(unit, dependencies.clone(), rustc); + let sbom = Sbom::new(unit, packages.clone(), rustc.clone()); let mut outfile = BufWriter::new(paths::create(output_path.clone())?); let output = serde_json::to_string_pretty(&sbom)?; @@ -164,29 +199,32 @@ pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Cargo /// Fetch all dependencies, including transitive ones. A dependency can also appear multiple times /// if it's included with different versions. -fn fetch_dependencies(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Vec { +fn fetch_packages(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Vec { let unit_graph = &build_runner.bcx.unit_graph; let root_deps = build_runner.unit_deps(unit); let mut result = Vec::new(); let mut queue: BTreeSet<&UnitDep> = root_deps.iter().collect(); - let mut visited: BTreeSet<&UnitDep> = BTreeSet::new(); + let mut visited = BTreeSet::new(); - while let Some(dependency) = queue.pop_first() { + while let Some(package) = queue.pop_first() { // ignore any custom build scripts. - if dependency.unit.mode.is_run_custom_build() { + if package.unit.mode.is_run_custom_build() { continue; } - if visited.contains(dependency) { + if visited.contains(package) { continue; } - result.push(dependency); - visited.insert(dependency); + let mut dependencies: BTreeSet<&UnitDep> = unit_graph[&package.unit].iter().collect(); + let sbom_dependencies = dependencies.iter().map(|dep| (*dep).into()).collect_vec(); + + result.push(SbomPackage::new(package, sbom_dependencies)); + + visited.insert(package); - let mut dependencies: BTreeSet<&UnitDep> = unit_graph[&dependency.unit].iter().collect(); queue.append(&mut dependencies); } - result.into_iter().map(|d| d.into()).collect_vec() + result } From d73c881e300303bbe35591e028a97b70a3ee0517 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 27 Feb 2024 16:40:59 +0100 Subject: [PATCH 05/22] Extract logic to fetch sbom output files This extracts the logic to get the list of SBOM output file paths into its own function in `BuildRunner` for a given Unit. --- src/cargo/core/compiler/build_runner/mod.rs | 15 +++++++++++++++ src/cargo/core/compiler/mod.rs | 6 ++++++ src/cargo/core/compiler/output_sbom.rs | 21 ++++++--------------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/cargo/core/compiler/build_runner/mod.rs b/src/cargo/core/compiler/build_runner/mod.rs index cc50789dce3..8b5a8e4f7c2 100644 --- a/src/cargo/core/compiler/build_runner/mod.rs +++ b/src/cargo/core/compiler/build_runner/mod.rs @@ -423,6 +423,21 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { self.files().metadata(unit) } + /// Returns the list of SBOM output file paths for a given [`Unit`]. + /// + /// Only call this function when `sbom` is active. + pub fn sbom_output_files(&self, unit: &Unit) -> CargoResult> { + assert!(self.bcx.build_config.sbom); + let files = self + .outputs(unit)? + .iter() + .filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable)) + .filter_map(|output_file| output_file.hardlink.as_ref()) + .map(|link_dst| link_dst.with_extension("cargo-sbom.json")) + .collect::>(); + Ok(files) + } + pub fn is_primary_package(&self, unit: &Unit) -> bool { self.primary_packages.contains(&unit.pkg.package_id()) } diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 1372e97c839..00e2be1696e 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -676,6 +676,7 @@ where fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult { let is_primary = build_runner.is_primary_package(unit); let is_workspace = build_runner.bcx.ws.is_member(&unit.pkg); + let sbom = build_runner.bcx.build_config.sbom; let mut base = build_runner .compilation @@ -692,6 +693,11 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult if is_primary { base.env("CARGO_PRIMARY_PACKAGE", "1"); + + if sbom { + let file_list = std::env::join_paths(build_runner.sbom_output_files(unit)?)?; + base.env("CARGO_SBOM_PATH", file_list); + } } if unit.target.is_test() || unit.target.is_bench() { diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs index 38b63038b21..d0e5d477549 100644 --- a/src/cargo/core/compiler/output_sbom.rs +++ b/src/cargo/core/compiler/output_sbom.rs @@ -13,7 +13,7 @@ use serde::Serialize; use crate::core::{profiles::Profile, PackageId}; use crate::core::{Target, TargetKind}; use crate::util::Rustc; -use crate::{core::compiler::FileFlavor, CargoResult}; +use crate::CargoResult; use super::{unit_graph::UnitDep, BuildRunner, CrateType, Unit}; @@ -177,21 +177,12 @@ pub fn output_sbom(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Cargo let packages = fetch_packages(build_runner, unit); - // TODO collect build & unit data, then transform into JSON output - for output in build_runner - .outputs(unit)? - .iter() - .filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable)) - { - if let Some(ref link_dst) = output.hardlink { - let output_path = link_dst.with_extension("cargo-sbom.json"); - - let sbom = Sbom::new(unit, packages.clone(), rustc.clone()); + for sbom_output_file in build_runner.sbom_output_files(unit)? { + let sbom = Sbom::new(unit, packages.clone(), rustc.clone()); - let mut outfile = BufWriter::new(paths::create(output_path.clone())?); - let output = serde_json::to_string_pretty(&sbom)?; - write!(outfile, "{}", output)?; - } + let mut outfile = BufWriter::new(paths::create(sbom_output_file)?); + let output = serde_json::to_string(&sbom)?; + write!(outfile, "{}", output)?; } Ok(()) From 4f5d0d548735d49f9dc9abe6ae2e4014b3bf4e68 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 27 Feb 2024 17:36:59 +0100 Subject: [PATCH 06/22] Add test file to check sbom output * add test to check project with bin & lib * extract sbom config into helper function --- tests/testsuite/main.rs | 1 + tests/testsuite/sbom.rs | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/testsuite/sbom.rs diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 271d333e2ef..515fd2ef160 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -162,6 +162,7 @@ mod rustdoc_extern_html; mod rustdocflags; mod rustflags; mod rustup; +mod sbom; mod script; mod search; mod shell_quoting; diff --git a/tests/testsuite/sbom.rs b/tests/testsuite/sbom.rs new file mode 100644 index 00000000000..117ada595ad --- /dev/null +++ b/tests/testsuite/sbom.rs @@ -0,0 +1,75 @@ +//! Tests for cargo-sbom precursor files. + +use cargo_test_support::{basic_bin_manifest, cargo_test, project, ProjectBuilder}; + +fn configured_project() -> ProjectBuilder { + project().file( + ".cargo/config.toml", + r#" + [build] + sbom = true + "#, + ) +} + +#[cargo_test] +fn build_sbom_using_cargo_config() { + let p = project() + .file( + ".cargo/config.toml", + r#" + [build] + sbom = true + "#, + ) + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/main.rs", r#"fn main() {}"#) + .build(); + + p.cargo("build").run(); + + let file = p.bin("foo").with_extension("cargo-sbom.json"); + assert!(file.is_file()); +} + +#[cargo_test] +fn build_sbom_using_env_var() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", r#"fn main() {}"#) + .build(); + + p.cargo("build").env("CARGO_BUILD_SBOM", "true").run(); + + let file = p.bin("foo").with_extension("cargo-sbom.json"); + assert!(file.is_file()); +} + +#[cargo_test] +fn build_sbom_project_bin_and_lib() { + let p = configured_project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.2.3" + authors = [] + + [lib] + crate-type = ["rlib"] + "#, + ) + .file("src/main.rs", r#"fn main() { let _i = foo::give_five(); }"#) + .file("src/lib.rs", r#"pub fn give_five() -> i32 { 5 }"#) + .build(); + + p.cargo("build").stream().run(); + + assert!(p.bin("foo").with_extension("cargo-sbom.json").is_file()); + assert_eq!( + 1, + p.glob(p.target_debug_dir().join("libfoo.cargo-sbom.json")) + .count() + ); +} From 81eee501ab94ed27f3c753cfb3678827f932ee59 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 11 Mar 2024 10:28:37 +0100 Subject: [PATCH 07/22] Add build type to dependency --- src/cargo/core/compiler/output_sbom.rs | 29 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/cargo/core/compiler/output_sbom.rs b/src/cargo/core/compiler/output_sbom.rs index d0e5d477549..99e4b30c4c3 100644 --- a/src/cargo/core/compiler/output_sbom.rs +++ b/src/cargo/core/compiler/output_sbom.rs @@ -17,6 +17,15 @@ use crate::CargoResult; use super::{unit_graph::UnitDep, BuildRunner, CrateType, Unit}; +#[derive(Serialize, Clone, Debug, Copy)] +#[serde(rename_all = "kebab-case")] +enum SbomBuildType { + /// A package dependency + Normal, + /// A build script dependency + Build, +} + /// Typed version of a SBOM format version number. pub struct SbomFormatVersion; @@ -65,12 +74,17 @@ struct SbomPackage { package: String, version: String, features: Vec, + build_type: SbomBuildType, extern_crate_name: String, dependencies: Vec, } impl SbomPackage { - pub fn new(dep: &UnitDep, dependencies: Vec) -> Self { + pub fn new( + dep: &UnitDep, + dependencies: Vec, + build_type: SbomBuildType, + ) -> Self { let package_id = dep.unit.pkg.package_id(); let package = package_id.name().to_string(); let version = package_id.version().to_string(); @@ -86,6 +100,7 @@ impl SbomPackage { package, version, features, + build_type, extern_crate_name: dep.extern_crate_name.to_string(), dependencies, } @@ -199,18 +214,20 @@ fn fetch_packages(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Vec = unit_graph[&package.unit].iter().collect(); let sbom_dependencies = dependencies.iter().map(|dep| (*dep).into()).collect_vec(); - result.push(SbomPackage::new(package, sbom_dependencies)); + result.push(SbomPackage::new(package, sbom_dependencies, build_type)); visited.insert(package); From f0af7f6b49f3af1df66ff34511df64d3d2523ab4 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 11 Mar 2024 12:02:02 +0100 Subject: [PATCH 08/22] Add test to read JSON Still needs to check output. --- tests/testsuite/sbom.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/testsuite/sbom.rs b/tests/testsuite/sbom.rs index 117ada595ad..c69800c928d 100644 --- a/tests/testsuite/sbom.rs +++ b/tests/testsuite/sbom.rs @@ -73,3 +73,29 @@ fn build_sbom_project_bin_and_lib() { .count() ); } + +#[cargo_test] +fn build_sbom_with_simple_build_script() { + let p = configured_project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + build = "build.rs" + "#, + ) + .file("src/main.rs", "#[cfg(foo)] fn main() {}") + .file( + "build.rs", + r#"fn main() { println!("cargo::rustc-cfg=foo"); }"#, + ) + .build(); + + p.cargo("build").stream().run(); + + let path = p.bin("foo").with_extension("cargo-sbom.json"); + assert!(path.is_file()); +} From bbe26068a352bbc28dd7d2eae6b76c5b39f7b1fb Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 11 Mar 2024 14:23:43 +0100 Subject: [PATCH 09/22] Guard sbom logic behind unstable feature --- src/cargo/core/compiler/build_config.rs | 5 +++++ src/cargo/core/compiler/mod.rs | 6 +++--- src/cargo/core/features.rs | 4 +++- src/cargo/util/context/mod.rs | 1 + tests/testsuite/cargo/z_help/stdout.term.svg | 22 +++++++++++--------- tests/testsuite/sbom.rs | 17 +++++++++++---- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/cargo/core/compiler/build_config.rs b/src/cargo/core/compiler/build_config.rs index a397d2a924f..d83feefaa9f 100644 --- a/src/cargo/core/compiler/build_config.rs +++ b/src/cargo/core/compiler/build_config.rs @@ -104,11 +104,16 @@ impl BuildConfig { anyhow::bail!("-Zbuild-std requires --target"); } + // If sbom flag is set, it requires the unstable feature let sbom = match gctx.get_env_os("CARGO_BUILD_SBOM") { Some(sbom) => sbom == "true", None => cfg.sbom == Some(true), }; + if sbom && !gctx.cli_unstable().sbom { + anyhow::bail!("Cargo build config 'sbom' is unstable; pass `-Zsbom` to enable it"); + } + Ok(BuildConfig { requested_kinds, jobs, diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 00e2be1696e..e8029c552f5 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -674,9 +674,9 @@ where /// completion of other units will be added later in runtime, such as flags /// from build scripts. fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult { + let gctx = build_runner.bcx.gctx; let is_primary = build_runner.is_primary_package(unit); let is_workspace = build_runner.bcx.ws.is_member(&unit.pkg); - let sbom = build_runner.bcx.build_config.sbom; let mut base = build_runner .compilation @@ -687,14 +687,14 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult build_deps_args(&mut base, build_runner, unit)?; add_cap_lints(build_runner.bcx, unit, &mut base); base.args(&unit.rustflags); - if build_runner.bcx.gctx.cli_unstable().binary_dep_depinfo { + if gctx.cli_unstable().binary_dep_depinfo { base.arg("-Z").arg("binary-dep-depinfo"); } if is_primary { base.env("CARGO_PRIMARY_PACKAGE", "1"); - if sbom { + if gctx.cli_unstable().sbom && build_runner.bcx.build_config.sbom { let file_list = std::env::join_paths(build_runner.sbom_output_files(unit)?)?; base.env("CARGO_SBOM_PATH", file_list); } diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index c44b8d51eb4..7e777870189 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -781,6 +781,7 @@ unstable_cli_options!( publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"), rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"), rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"), + sbom: bool = ("Enable the `sbom` option in build config in .cargo/config.toml file"), script: bool = ("Enable support for single-file, `.rs` packages"), separate_nightlies: bool, skip_rustdoc_fingerprint: bool, @@ -1285,9 +1286,10 @@ impl CliUnstable { "publish-timeout" => self.publish_timeout = parse_empty(k, v)?, "rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?, "rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?, + "sbom" => self.sbom = parse_empty(k, v)?, + "script" => self.script = parse_empty(k, v)?, "separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?, "skip-rustdoc-fingerprint" => self.skip_rustdoc_fingerprint = parse_empty(k, v)?, - "script" => self.script = parse_empty(k, v)?, "target-applies-to-host" => self.target_applies_to_host = parse_empty(k, v)?, "unstable-options" => self.unstable_options = parse_empty(k, v)?, _ => bail!("\ diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 23084d086ef..56520220892 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2615,6 +2615,7 @@ pub struct CargoBuildConfig { // deprecated alias for artifact-dir pub out_dir: Option, pub artifact_dir: Option, + /// Unstable feature `-Zsbom`. pub sbom: Option, } diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index e5386620e46..1d44d23fb8e 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +