Skip to main content

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 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::core::TargetKind;
14use crate::util::Rustc;
15use crate::util::interning::InternedString;
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`].
100pub fn build_sbom(build_runner: &BuildRunner<'_, '_>, root: &Unit) -> CargoResult<Sbom> {
101    let bcx = build_runner.bcx;
102    let rustc: SbomRustc = bcx.rustc().into();
103
104    let mut crates = Vec::new();
105    let sbom_graph = build_sbom_graph(build_runner, root);
106
107    // Build set of indices for each node in the graph for fast lookup.
108    let indices: HashMap<&Unit, SbomIndex> = sbom_graph
109        .keys()
110        .enumerate()
111        .map(|(i, dep)| (*dep, SbomIndex(i)))
112        .collect();
113
114    // Add a item to the crates list for each node in the graph.
115    for (unit, edges) in sbom_graph {
116        let mut krate = SbomCrate::new(unit);
117        for (dep, kind) in edges {
118            krate.dependencies.push(SbomDependency {
119                index: indices[dep],
120                kind: kind,
121            });
122        }
123        crates.push(krate);
124    }
125    let target = match root.kind {
126        super::CompileKind::Host => build_runner.bcx.host_triple(),
127        super::CompileKind::Target(target) => target.rustc_target(),
128    };
129    Ok(Sbom {
130        version: SbomFormatVersion(1),
131        crates,
132        root: indices[root],
133        rustc,
134        target,
135    })
136}
137
138/// List all dependencies, including transitive ones. A dependency can also appear multiple times
139/// if it's using different settings, e.g. profile, features or crate versions.
140///
141/// Returns a graph of dependencies.
142fn build_sbom_graph<'a>(
143    build_runner: &'a BuildRunner<'_, '_>,
144    root: &'a Unit,
145) -> BTreeMap<&'a Unit, BTreeSet<(&'a Unit, SbomDependencyType)>> {
146    tracing::trace!("building sbom graph for {}", root.pkg.package_id());
147
148    let mut queue = Vec::new();
149    let mut sbom_graph: BTreeMap<&Unit, BTreeSet<(&Unit, SbomDependencyType)>> = BTreeMap::new();
150    let mut visited = HashSet::default();
151
152    // Search to collect all dependencies of the root unit.
153    queue.push((root, root, false));
154    while let Some((node, parent, is_build_dep)) = queue.pop() {
155        let dependencies = sbom_graph.entry(parent).or_default();
156        for dep in build_runner.unit_deps(node) {
157            let dep = &dep.unit;
158            let (next_parent, next_is_build_dep) = if dep.mode == CompileMode::RunCustomBuild {
159                // Nodes in the SBOM graph for building/running build scripts are moved on to their parent as build dependencies.
160                (parent, true)
161            } else {
162                // Proc-macros and build scripts are marked as build dependencies.
163                let dep_type = match is_build_dep || dep.target.proc_macro() {
164                    false => SbomDependencyType::Normal,
165                    true => SbomDependencyType::Build,
166                };
167                dependencies.insert((dep, dep_type));
168                tracing::trace!(
169                    "adding sbom edge {} -> {} ({:?})",
170                    parent.pkg.package_id(),
171                    dep.pkg.package_id(),
172                    dep_type,
173                );
174                (dep, false)
175            };
176            if visited.insert(dep) {
177                queue.push((dep, next_parent, next_is_build_dep));
178            }
179        }
180    }
181    sbom_graph
182}