Skip to main content

cargo/compiler/
output_sbom.rs

1//! cargo-sbom precursor files for external tools to create SBOM files from.
2//! See [`build_sbom_graph`] for more.
3
4use crate::util::data_structures::{HashMap, HashSet};
5use std::collections::{BTreeMap, BTreeSet};
6use std::path::PathBuf;
7
8use cargo_util_schemas::core::PackageIdSpec;
9use itertools::Itertools;
10use serde::Serialize;
11
12use crate::CargoResult;
13use crate::util::Rustc;
14use crate::util::interning::InternedString;
15use crate::workspace::TargetKind;
16
17use super::{BuildRunner, CompileMode, Unit};
18
19/// Typed version of a SBOM format version number.
20#[derive(Serialize, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
21pub struct SbomFormatVersion(u32);
22
23#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize)]
24#[serde(rename_all = "snake_case")]
25enum SbomDependencyType {
26    /// A dependency linked to the artifact produced by this unit.
27    Normal,
28    /// A dependency needed to run the build for this unit (e.g. a build script or proc-macro).
29    /// The dependency is not linked to the artifact produced by this unit.
30    Build,
31}
32
33#[derive(Serialize, Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
34struct SbomIndex(usize);
35
36#[derive(Serialize, Clone, Debug)]
37#[serde(rename_all = "snake_case")]
38struct SbomDependency {
39    index: SbomIndex,
40    kind: SbomDependencyType,
41}
42
43#[derive(Serialize, Clone, Debug)]
44#[serde(rename_all = "snake_case")]
45struct SbomCrate {
46    id: PackageIdSpec,
47    features: Vec<String>,
48    dependencies: Vec<SbomDependency>,
49    kind: TargetKind,
50}
51
52impl SbomCrate {
53    pub fn new(unit: &Unit) -> Self {
54        let package_id = unit.pkg.package_id().to_spec();
55        let features = unit.features.iter().map(|f| f.to_string()).collect_vec();
56        Self {
57            id: package_id,
58            features,
59            dependencies: Vec::new(),
60            kind: unit.target.kind().clone(),
61        }
62    }
63}
64
65#[derive(Serialize, Clone)]
66#[serde(rename_all = "snake_case")]
67struct SbomRustc {
68    version: String,
69    wrapper: Option<PathBuf>,
70    workspace_wrapper: Option<PathBuf>,
71    commit_hash: Option<String>,
72    host: String,
73    verbose_version: String,
74}
75
76impl From<&Rustc> for SbomRustc {
77    fn from(rustc: &Rustc) -> Self {
78        Self {
79            version: rustc.version.to_string(),
80            wrapper: rustc.wrapper.clone(),
81            workspace_wrapper: rustc.workspace_wrapper.clone(),
82            commit_hash: rustc.commit_hash.clone(),
83            host: rustc.host.to_string(),
84            verbose_version: rustc.verbose_version.clone(),
85        }
86    }
87}
88
89#[derive(Serialize)]
90#[serde(rename_all = "snake_case")]
91pub struct Sbom {
92    version: SbomFormatVersion,
93    root: SbomIndex,
94    crates: Vec<SbomCrate>,
95    rustc: SbomRustc,
96    target: InternedString,
97}
98
99/// Build an [`Sbom`] for the given [`Unit`].
100#[tracing::instrument(skip_all)]
101pub fn build_sbom(build_runner: &BuildRunner<'_, '_>, root: &Unit) -> CargoResult<Sbom> {
102    let bcx = build_runner.bcx;
103    let rustc: SbomRustc = bcx.rustc().into();
104
105    let mut crates = Vec::new();
106    let sbom_graph = build_sbom_graph(build_runner, root);
107
108    // Build set of indices for each node in the graph for fast lookup.
109    let indices: HashMap<&Unit, SbomIndex> = sbom_graph
110        .keys()
111        .enumerate()
112        .map(|(i, dep)| (*dep, SbomIndex(i)))
113        .collect();
114
115    // Add a item to the crates list for each node in the graph.
116    for (unit, edges) in sbom_graph {
117        let mut krate = SbomCrate::new(unit);
118        for (dep, kind) in edges {
119            krate.dependencies.push(SbomDependency {
120                index: indices[dep],
121                kind: kind,
122            });
123        }
124        crates.push(krate);
125    }
126    let target = match root.kind {
127        super::CompileKind::Host => build_runner.bcx.host_triple(),
128        super::CompileKind::Target(target) => target.rustc_target(),
129    };
130    Ok(Sbom {
131        version: SbomFormatVersion(1),
132        crates,
133        root: indices[root],
134        rustc,
135        target,
136    })
137}
138
139/// List all dependencies, including transitive ones. A dependency can also appear multiple times
140/// if it's using different settings, e.g. profile, features or crate versions.
141///
142/// Returns a graph of dependencies.
143#[tracing::instrument(skip_all)]
144fn build_sbom_graph<'a>(
145    build_runner: &'a BuildRunner<'_, '_>,
146    root: &'a Unit,
147) -> BTreeMap<&'a Unit, BTreeSet<(&'a Unit, SbomDependencyType)>> {
148    tracing::trace!("building sbom graph for {}", root.pkg.package_id());
149
150    let mut queue = Vec::new();
151    let mut sbom_graph: BTreeMap<&Unit, BTreeSet<(&Unit, SbomDependencyType)>> = BTreeMap::new();
152    let mut visited = HashSet::default();
153
154    // Search to collect all dependencies of the root unit.
155    queue.push((root, root, false));
156    while let Some((node, parent, is_build_dep)) = queue.pop() {
157        let dependencies = sbom_graph.entry(parent).or_default();
158        for dep in build_runner.unit_deps(node) {
159            let dep = &dep.unit;
160            let (next_parent, next_is_build_dep) = if dep.mode == CompileMode::RunCustomBuild {
161                // Nodes in the SBOM graph for building/running build scripts are moved on to their parent as build dependencies.
162                (parent, true)
163            } else {
164                // Proc-macros and build scripts are marked as build dependencies.
165                let dep_type = match is_build_dep || dep.target.proc_macro() {
166                    false => SbomDependencyType::Normal,
167                    true => SbomDependencyType::Build,
168                };
169                dependencies.insert((dep, dep_type));
170                (dep, false)
171            };
172            if visited.insert(dep) {
173                queue.push((dep, next_parent, next_is_build_dep));
174            }
175        }
176    }
177    sbom_graph
178}