Skip to content

Commit e38cf70

Browse files
committed
feat: Add SBOM pre-cursor files
Adds a new option `build.sbom` that adds generation of a JSON file containing dependency information alongside compiled artifacts.
1 parent 5946db3 commit e38cf70

File tree

13 files changed

+970
-15
lines changed

13 files changed

+970
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cargo-test-support/Cargo.toml

Lines changed: 1 addition & 1 deletion
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.84" # MSRV:1
66
license.workspace = true

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)