#
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::CargoResult;
    12use crate::core::TargetKind;
    13use crate::util::Rustc;
    14use crate::util::interning::InternedString;
    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 indices for each node in the graph for fast lookup.
    107    let indices: 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: indices[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: indices[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}