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