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 cargo_util::collections::{HashMap, HashSet};
5use std::collections::{BTreeMap, BTreeSet};
6
7use cargo_util_schemas::sbom::{
8    Sbom, SbomCrate, SbomDependency, SbomDependencyType, SbomFormatVersion, SbomIndex, SbomRustc,
9    SbomTargetKind,
10};
11use itertools::Itertools;
12
13use crate::CargoResult;
14use crate::util::Rustc;
15use crate::workspace::TargetKind;
16
17use super::{BuildRunner, CompileMode, Unit};
18
19impl From<TargetKind> for SbomTargetKind {
20    fn from(t: TargetKind) -> Self {
21        use TargetKind::*;
22        match t {
23            Lib(c) => SbomTargetKind::Lib(c),
24            Bin => SbomTargetKind::Bin,
25            Test => SbomTargetKind::Test,
26            Bench => SbomTargetKind::Bench,
27            ExampleBin | ExampleLib(_) => SbomTargetKind::Example,
28            CustomBuild => SbomTargetKind::CustomBuild,
29        }
30    }
31}
32
33impl From<&Rustc> for SbomRustc {
34    fn from(rustc: &Rustc) -> Self {
35        Self {
36            version: rustc.version.to_string(),
37            wrapper: rustc.wrapper.clone(),
38            workspace_wrapper: rustc.workspace_wrapper.clone(),
39            commit_hash: rustc.commit_hash.clone(),
40            host: rustc.host.to_string(),
41            verbose_version: rustc.verbose_version.clone(),
42        }
43    }
44}
45
46/// Build an [`Sbom`] for the given [`Unit`].
47#[tracing::instrument(skip_all)]
48pub fn build_sbom(build_runner: &BuildRunner<'_, '_>, root: &Unit) -> CargoResult<Sbom> {
49    let bcx = build_runner.bcx;
50    let rustc: SbomRustc = bcx.rustc().into();
51
52    let mut crates = Vec::new();
53    let sbom_graph = build_sbom_graph(build_runner, root);
54
55    // Build set of indices for each node in the graph for fast lookup.
56    let indices: HashMap<&Unit, SbomIndex> = sbom_graph
57        .keys()
58        .enumerate()
59        .map(|(i, dep)| (*dep, SbomIndex(i)))
60        .collect();
61
62    // Add a item to the crates list for each node in the graph.
63    for (unit, edges) in sbom_graph {
64        let package_id = unit.pkg.package_id().to_spec();
65        let features = unit.features.iter().map(|f| f.to_string()).collect_vec();
66        let target_kind = unit.target.kind().clone();
67
68        let mut krate = SbomCrate::new(package_id, features, SbomTargetKind::from(target_kind));
69        for (dep, kind) in edges {
70            krate.dependencies.push(SbomDependency {
71                index: indices[dep],
72                kind: kind,
73            });
74        }
75        crates.push(krate);
76    }
77    let target = match root.kind {
78        super::CompileKind::Host => build_runner.bcx.host_triple(),
79        super::CompileKind::Target(target) => target.rustc_target(),
80    };
81    Ok(Sbom {
82        version: SbomFormatVersion(1),
83        crates,
84        root: indices[root],
85        rustc,
86        target: target.to_string(),
87    })
88}
89
90/// List all dependencies, including transitive ones. A dependency can also appear multiple times
91/// if it's using different settings, e.g. profile, features or crate versions.
92///
93/// Returns a graph of dependencies.
94#[tracing::instrument(skip_all)]
95fn build_sbom_graph<'a>(
96    build_runner: &'a BuildRunner<'_, '_>,
97    root: &'a Unit,
98) -> BTreeMap<&'a Unit, BTreeSet<(&'a Unit, SbomDependencyType)>> {
99    tracing::trace!("building sbom graph for {}", root.pkg.package_id());
100
101    let mut queue = Vec::new();
102    let mut sbom_graph: BTreeMap<&Unit, BTreeSet<(&Unit, SbomDependencyType)>> = BTreeMap::new();
103    let mut visited = HashSet::default();
104
105    // Search to collect all dependencies of the root unit.
106    queue.push((root, root, false));
107    while let Some((node, parent, is_build_dep)) = queue.pop() {
108        let dependencies = sbom_graph.entry(parent).or_default();
109        for dep in build_runner.unit_deps(node) {
110            let dep = &dep.unit;
111            let (next_parent, next_is_build_dep) = if dep.mode == CompileMode::RunCustomBuild {
112                // Nodes in the SBOM graph for building/running build scripts are moved on to their parent as build dependencies.
113                (parent, true)
114            } else {
115                // Proc-macros and build scripts are marked as build dependencies.
116                let dep_type = match is_build_dep || dep.target.proc_macro() {
117                    false => SbomDependencyType::Normal,
118                    true => SbomDependencyType::Build,
119                };
120                dependencies.insert((dep, dep_type));
121                (dep, false)
122            };
123            if visited.insert(dep) {
124                queue.push((dep, next_parent, next_is_build_dep));
125            }
126        }
127    }
128    sbom_graph
129}