Skip to content

Commit 7ea222d

Browse files
authored
feat: implement RFC 3553 to add SBOM support (#13709)
### What does this PR try to resolve? This PR is an implementation of [RFC 3553] to add support to generate pre-cursor SBOM files for compiled artifacts in Cargo. ### How should we test and review this PR? The [RFC 3553] adds a new option to Cargo to emit SBOM pre-cursor files. A project can be configured either by the new Cargo config field `sbom`. ```toml # .cargo/config.toml [build] sbom = true ``` or using the environment variable `CARGO_BUILD_SBOM=true`. The `sbom` option is an unstable feature and requires the `-Zsbom` flag to enable it. Check out this branch & compile Cargo. Pick a Cargo project to test it on, then run: ``` CARGO_BUILD_SBOM=true <path/to/compiled/cargo>/target/debug/cargo build -Zsbom ``` All generated `*.cargo-sbom.json` files are located in the `target` folder alongside their artifacts. To list all generated files use: ``` find ./target -name "*.cargo-sbom.json" ``` then check their content. To see the current output format, see [these examples](https://gist.github.com/justahero/2683f5520bf5e921c6b839a0e91cd01c). ### What does the PR not solve? The PR leaves a task(s) open that are either out of scope or should be done in a follow-up PRs. * does not address #6313 (see [comment](#13709 (comment))) ### Additional information There are a few things that I would like to get feedback on, in particular the generated JSON format is not final. Currently it holds the information listed in the [RFC 3553], but it could be further enriched with information only available during builds. During the implementation a number of questions arose: - [ ] Should the graph be packages or crates? - The unit graph that the SBOM is based on is units. The current SBOM graph is identical to the unit graph, with the run build script nodes merged with building build scripts. - Artifact dependencies may impact this - [ ] Which outputs should get SBOMs files? - Currently: executables (including examples and tests), dylib, cdylib, staticlib - [ ] How do we refer to "normal" dependencies? #13709 (comment) - [ ] What case should we use? #13709 (comment) - [ ] Should this be `build.sbom` or `profile.*.sbom` - [ ] Is sbom the right name for this? Thanks @arlosi, @RobJellinghaus and @lfrancke for initial guidance & feedback. * RFC: #rust-lang/rfcs/pull/3553 [RFC 3553]: rust-lang/rfcs#3553
2 parents 392d68b + 5f833db commit 7ea222d

File tree

17 files changed

+1099
-23
lines changed

17 files changed

+1099
-23
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cargo-test-support/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cargo-test-support"
3-
version = "0.7.2"
3+
version = "0.7.3"
44
edition.workspace = true
55
rust-version = "1.85" # MSRV:1
66
license.workspace = true

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

+10
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

+14
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_context/target_info.rs

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub enum FileFlavor {
7272
Rmeta,
7373
/// Piece of external debug information (e.g., `.dSYM`/`.pdb` file).
7474
DebugInfo,
75+
/// SBOM (Software Bill of Materials pre-cursor) file (e.g. cargo-sbon.json).
76+
Sbom,
7577
}
7678

7779
/// Type of each file generated by a Unit.

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -495,13 +495,37 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
495495
CompileMode::Test
496496
| CompileMode::Build
497497
| CompileMode::Bench
498-
| CompileMode::Check { .. } => self.calc_outputs_rustc(unit, bcx)?,
498+
| CompileMode::Check { .. } => {
499+
let mut outputs = self.calc_outputs_rustc(unit, bcx)?;
500+
if bcx.build_config.sbom && bcx.gctx.cli_unstable().sbom {
501+
let sbom_files: Vec<_> = outputs
502+
.iter()
503+
.filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable))
504+
.map(|output| OutputFile {
505+
path: Self::append_sbom_suffix(&output.path),
506+
hardlink: output.hardlink.as_ref().map(Self::append_sbom_suffix),
507+
export_path: output.export_path.as_ref().map(Self::append_sbom_suffix),
508+
flavor: FileFlavor::Sbom,
509+
})
510+
.collect();
511+
outputs.extend(sbom_files.into_iter());
512+
}
513+
outputs
514+
}
499515
};
500516
debug!("Target filenames: {:?}", ret);
501517

502518
Ok(Arc::new(ret))
503519
}
504520

521+
/// Append the SBOM suffix to the file name.
522+
fn append_sbom_suffix(link: &PathBuf) -> PathBuf {
523+
const SBOM_FILE_EXTENSION: &str = ".cargo-sbom.json";
524+
let mut link_buf = link.clone().into_os_string();
525+
link_buf.push(SBOM_FILE_EXTENSION);
526+
PathBuf::from(link_buf)
527+
}
528+
505529
/// Computes the actual, full pathnames for all the files generated by rustc.
506530
///
507531
/// The `OutputFile` also contains the paths where those files should be

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,10 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
309309

310310
fn collect_tests_and_executables(&mut self, unit: &Unit) -> CargoResult<()> {
311311
for output in self.outputs(unit)?.iter() {
312-
if output.flavor == FileFlavor::DebugInfo || output.flavor == FileFlavor::Auxiliary {
312+
if matches!(
313+
output.flavor,
314+
FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom
315+
) {
313316
continue;
314317
}
315318

@@ -446,6 +449,16 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
446449
self.files().metadata(unit).unit_id()
447450
}
448451

452+
/// Returns the list of SBOM output file paths for a given [`Unit`].
453+
pub fn sbom_output_files(&self, unit: &Unit) -> CargoResult<Vec<PathBuf>> {
454+
Ok(self
455+
.outputs(unit)?
456+
.iter()
457+
.filter(|o| o.flavor == FileFlavor::Sbom)
458+
.map(|o| o.path.clone())
459+
.collect())
460+
}
461+
449462
pub fn is_primary_package(&self, unit: &Unit) -> bool {
450463
self.primary_packages.contains(&unit.pkg.package_id())
451464
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1517,7 +1517,12 @@ fn calculate_normal(
15171517
let outputs = build_runner
15181518
.outputs(unit)?
15191519
.iter()
1520-
.filter(|output| !matches!(output.flavor, FileFlavor::DebugInfo | FileFlavor::Auxiliary))
1520+
.filter(|output| {
1521+
!matches!(
1522+
output.flavor,
1523+
FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom
1524+
)
1525+
})
15211526
.map(|output| output.path.clone())
15221527
.collect();
15231528

src/cargo/core/compiler/mod.rs

+15-2
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() {

src/cargo/core/compiler/output_depinfo.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,12 @@ pub fn output_depinfo(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Ca
141141
.map(|f| render_filename(f, basedir))
142142
.collect::<CargoResult<Vec<_>>>()?;
143143

144-
for output in build_runner
145-
.outputs(unit)?
146-
.iter()
147-
.filter(|o| !matches!(o.flavor, FileFlavor::DebugInfo | FileFlavor::Auxiliary))
148-
{
144+
for output in build_runner.outputs(unit)?.iter().filter(|o| {
145+
!matches!(
146+
o.flavor,
147+
FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom
148+
)
149+
}) {
149150
if let Some(ref link_dst) = output.hardlink {
150151
let output_path = link_dst.with_extension("d");
151152
if success {

0 commit comments

Comments
 (0)