Skip to content

Commit d453ce3

Browse files
committed
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 Output source, profile & dependencies Trying to fetch all dependencies This ignores dependencies for custom build scripts. The output should be similar to what `cargo tree` reports. Output package dependencies This is similar to what the `cargo metadata` command outputs. 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. Add test file to check sbom output * add test to check project with bin & lib * extract sbom config into helper function Add build type to dependency Add test to read JSON Still needs to check output. Guard sbom logic behind unstable feature Add test with custom build script Integrate review feedback * disable `sbom` config when `-Zsbom` is not passed as unstable option * refactor tests * add test Expand end-to-end tests This expands the tests to reflect end-to-end tests by comparing the generated JSON output files with expected strings. * add test helper to compare actual & expected JSON content * refactor setup of packages in test Add 'sbom' section to unstable features doc Append SBOM file suffix instead of replacing Instead of replacing the file extension, the `.cargo-sbom.json` suffix is appended to the output file. This is to keep existing file extensions in place. * refactor logic to set `sbom` property from build config * expand build script related test to check JSON output Integrate review feedback * use `PackageIdSpec` instead of only `PackageId` in SBOM output * change `version` of a dependency to `Option<Version>` * output `Vec<CrateType>` instead of only the first found crate type * output rustc workspace wrapper * update 'warning' string in test using `[WARNING]` * use `serde_json::to_writer` to serialize SBOM * set sbom suffix in tests explicitely, instead of using `with_extension` Output additional fields to JSON In case a unit's profile differs from the profile information on root level, it's added to the package information to the JSON output. The verbose output for `rustc -vV` is also written to the `rustc` field in the SBOM. * rename `fetch_packages` to `collect_packages` * update JSON in tests to include profile information Add test to check multiple crate types Add test to check artifact name conflict Use SbomProfile to wrap Profile type This adds the `SbomProfile` to convert the existing `Profile` into, to expose relevant fields. For now it removes the `strip` field, while serializing all other fields. It should keep the output consistent, even when fields in the `Profile` change, e.g. new field added. Document package profile * only export `profile` field in case it differs from root profile Add test to check different features The added test uses a crate with multiple features. The main crate uses the dependency in the normal build & the custom build script with different features. Refactor storing of package dependencies All dependencies for a package are indices into the `packages` list now. This sets the correct association between a dependency & its associated package. * remove `SbomDependency` struct Refactor tests to use snapbox
1 parent 684bca2 commit d453ce3

File tree

11 files changed

+860
-2
lines changed

11 files changed

+860
-2
lines changed

crates/cargo-test-support/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,16 @@ impl Project {
415415
.join(paths::get_lib_filename(name, kind))
416416
}
417417

418+
/// Path to a dynamic library.
419+
/// ex: `/path/to/cargo/target/cit/t0/foo/target/debug/examples/libex.dylib`
420+
pub fn dylib(&self, name: &str) -> PathBuf {
421+
self.target_debug_dir().join(format!(
422+
"{}{name}{}",
423+
env::consts::DLL_PREFIX,
424+
env::consts::DLL_SUFFIX
425+
))
426+
}
427+
418428
/// Path to a debug binary.
419429
///
420430
/// ex: `$CARGO_TARGET_TMPDIR/cit/t0/foo/target/debug/foo`

src/cargo/core/compiler/build_config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub struct BuildConfig {
4848
pub future_incompat_report: bool,
4949
/// Which kinds of build timings to output (empty if none).
5050
pub timing_outputs: Vec<TimingOutput>,
51+
/// Output SBOM precursor files.
52+
pub sbom: bool,
5153
}
5254

5355
fn default_parallelism() -> CargoResult<u32> {
@@ -99,6 +101,17 @@ impl BuildConfig {
99101
},
100102
};
101103

104+
// If sbom flag is set, it requires the unstable feature
105+
let sbom = match (cfg.sbom, gctx.cli_unstable().sbom) {
106+
(Some(sbom), true) => sbom,
107+
(Some(_), false) => {
108+
gctx.shell()
109+
.warn("ignoring 'sbom' config, pass `-Zsbom` to enable it")?;
110+
false
111+
}
112+
(None, _) => false,
113+
};
114+
102115
Ok(BuildConfig {
103116
requested_kinds,
104117
jobs,
@@ -115,6 +128,7 @@ impl BuildConfig {
115128
export_dir: None,
116129
future_incompat_report: false,
117130
timing_outputs: Vec::new(),
131+
sbom,
118132
})
119133
}
120134

src/cargo/core/compiler/build_runner/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! [`BuildRunner`] is the mutable state used during the build process.
22
33
use std::collections::{BTreeSet, HashMap, HashSet};
4+
use std::io::BufWriter;
45
use std::path::{Path, PathBuf};
56
use std::sync::{Arc, Mutex};
67

@@ -10,6 +11,7 @@ use crate::core::PackageId;
1011
use crate::util::cache_lock::CacheLockMode;
1112
use crate::util::errors::CargoResult;
1213
use anyhow::{bail, Context as _};
14+
use cargo_util::paths;
1315
use filetime::FileTime;
1416
use itertools::Itertools;
1517
use jobserver::Client;
@@ -291,6 +293,14 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
291293
}
292294

293295
super::output_depinfo(&mut self, unit)?;
296+
297+
if self.bcx.build_config.sbom {
298+
let sbom = super::build_sbom(&mut self, unit)?;
299+
for sbom_output_file in self.sbom_output_files(unit)? {
300+
let outfile = BufWriter::new(paths::create(sbom_output_file)?);
301+
serde_json::to_writer(outfile, &sbom)?;
302+
}
303+
}
294304
}
295305

296306
for (script_meta, output) in self.build_script_outputs.lock().unwrap().iter() {
@@ -446,6 +456,33 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
446456
self.files().metadata(unit).unit_id()
447457
}
448458

459+
/// Returns the list of SBOM output file paths for a given [`Unit`].
460+
pub fn sbom_output_files(&self, unit: &Unit) -> CargoResult<Vec<PathBuf>> {
461+
let mut sbom_files = Vec::new();
462+
if !self.bcx.build_config.sbom || !self.bcx.gctx.cli_unstable().sbom {
463+
return Ok(sbom_files);
464+
}
465+
for output in self.outputs(unit)?.iter() {
466+
if matches!(output.flavor, FileFlavor::Normal | FileFlavor::Linkable) {
467+
if let Some(path) = &output.hardlink {
468+
sbom_files.push(Self::append_sbom_suffix(path));
469+
}
470+
if let Some(path) = &output.export_path {
471+
sbom_files.push(Self::append_sbom_suffix(path));
472+
}
473+
}
474+
}
475+
Ok(sbom_files)
476+
}
477+
478+
/// Append the SBOM suffix to the file name.
479+
fn append_sbom_suffix(link: &PathBuf) -> PathBuf {
480+
const SBOM_FILE_EXTENSION: &str = ".cargo-sbom.json";
481+
let mut link_buf = link.clone().into_os_string();
482+
link_buf.push(SBOM_FILE_EXTENSION);
483+
PathBuf::from(link_buf)
484+
}
485+
449486
pub fn is_primary_package(&self, unit: &Unit) -> bool {
450487
self.primary_packages.contains(&unit.pkg.package_id())
451488
}

src/cargo/core/compiler/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub(crate) mod layout;
4747
mod links;
4848
mod lto;
4949
mod output_depinfo;
50+
mod output_sbom;
5051
pub mod rustdoc;
5152
pub mod standard_lib;
5253
mod timings;
@@ -60,7 +61,7 @@ use std::env;
6061
use std::ffi::{OsStr, OsString};
6162
use std::fmt::Display;
6263
use std::fs::{self, File};
63-
use std::io::{BufRead, Write};
64+
use std::io::{BufRead, BufWriter, Write};
6465
use std::path::{Path, PathBuf};
6566
use std::sync::Arc;
6667

@@ -85,6 +86,7 @@ use self::job_queue::{Job, JobQueue, JobState, Work};
8586
pub(crate) use self::layout::Layout;
8687
pub use self::lto::Lto;
8788
use self::output_depinfo::output_depinfo;
89+
use self::output_sbom::build_sbom;
8890
use self::unit_graph::UnitDep;
8991
use crate::core::compiler::future_incompat::FutureIncompatReport;
9092
pub use crate::core::compiler::unit::{Unit, UnitInterner};
@@ -307,6 +309,8 @@ fn rustc(
307309
let script_metadata = build_runner.find_build_script_metadata(unit);
308310
let is_local = unit.is_local();
309311
let artifact = unit.artifact;
312+
let sbom_files = build_runner.sbom_output_files(unit)?;
313+
let sbom = build_sbom(build_runner, unit)?;
310314

311315
let hide_diagnostics_for_scrape_unit = build_runner.bcx.unit_can_fail_for_docscraping(unit)
312316
&& !matches!(
@@ -392,6 +396,12 @@ fn rustc(
392396
if build_plan {
393397
state.build_plan(buildkey, rustc.clone(), outputs.clone());
394398
} else {
399+
for file in sbom_files {
400+
tracing::debug!("writing sbom to {}", file.display());
401+
let outfile = BufWriter::new(paths::create(&file)?);
402+
serde_json::to_writer(outfile, &sbom)?;
403+
}
404+
395405
let result = exec
396406
.exec(
397407
&rustc,
@@ -685,6 +695,7 @@ where
685695
/// completion of other units will be added later in runtime, such as flags
686696
/// from build scripts.
687697
fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult<ProcessBuilder> {
698+
let gctx = build_runner.bcx.gctx;
688699
let is_primary = build_runner.is_primary_package(unit);
689700
let is_workspace = build_runner.bcx.ws.is_member(&unit.pkg);
690701

@@ -700,7 +711,7 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult
700711
base.args(args);
701712
}
702713
base.args(&unit.rustflags);
703-
if build_runner.bcx.gctx.cli_unstable().binary_dep_depinfo {
714+
if gctx.cli_unstable().binary_dep_depinfo {
704715
base.arg("-Z").arg("binary-dep-depinfo");
705716
}
706717
if build_runner.bcx.gctx.cli_unstable().checksum_freshness {
@@ -709,6 +720,8 @@ fn prepare_rustc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResult
709720

710721
if is_primary {
711722
base.env("CARGO_PRIMARY_PACKAGE", "1");
723+
let file_list = std::env::join_paths(build_runner.sbom_output_files(unit)?)?;
724+
base.env("CARGO_SBOM_PATH", file_list);
712725
}
713726

714727
if unit.target.is_test() || unit.target.is_bench() {

0 commit comments

Comments
 (0)