Skip to content

Commit 6b262d3

Browse files
authored
Merge pull request #755 from thillux/strip-build-deps-2
more explicit build dependency handling
2 parents ebc5b4f + 52a8170 commit 6b262d3

File tree

15 files changed

+317
-32
lines changed

15 files changed

+317
-32
lines changed

cargo-cyclonedx/src/cli.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ Defaults to the host target, as printed by 'rustc -vV'"
100100
/// The CycloneDX specification version to output: `1.3`, `1.4` or `1.5`. Defaults to 1.3
101101
#[clap(long = "spec-version")]
102102
pub spec_version: Option<SpecVersion>,
103+
104+
/// List only dependencies of kind normal (no build deps, no dev deps)
105+
#[clap(name = "no-build-deps", long = "no-build-deps")]
106+
pub no_build_deps: bool,
103107
}
104108

105109
impl Args {
@@ -170,6 +174,7 @@ impl Args {
170174

171175
let describe = self.describe;
172176
let spec_version = self.spec_version;
177+
let only_normal_deps = Some(self.no_build_deps);
173178

174179
Ok(SbomConfig {
175180
format: self.format,
@@ -180,6 +185,7 @@ impl Args {
180185
license_parser,
181186
describe,
182187
spec_version,
188+
only_normal_deps,
183189
})
184190
}
185191
}
@@ -190,6 +196,11 @@ pub enum ArgsError {
190196
FilenameOverrideError(#[from] FilenameOverrideError),
191197
}
192198

199+
#[cfg(test)]
200+
pub fn parse_to_config(args: &[&str]) -> SbomConfig {
201+
Args::parse_from(args.iter()).as_config().unwrap()
202+
}
203+
193204
#[cfg(test)]
194205
mod tests {
195206
use super::*;
@@ -222,10 +233,6 @@ mod tests {
222233
assert!(!contains_feature(&config, ""));
223234
}
224235

225-
fn parse_to_config(args: &[&str]) -> SbomConfig {
226-
Args::parse_from(args.iter()).as_config().unwrap()
227-
}
228-
229236
fn contains_feature(config: &SbomConfig, feature: &str) -> bool {
230237
config
231238
.features

cargo-cyclonedx/src/config.rs

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub struct SbomConfig {
3333
pub license_parser: Option<LicenseParserOptions>,
3434
pub describe: Option<Describe>,
3535
pub spec_version: Option<SpecVersion>,
36+
pub only_normal_deps: Option<bool>,
3637
}
3738

3839
impl SbomConfig {
@@ -57,6 +58,7 @@ impl SbomConfig {
5758
.or_else(|| self.license_parser.clone()),
5859
describe: other.describe.or(self.describe),
5960
spec_version: other.spec_version.or(self.spec_version),
61+
only_normal_deps: other.only_normal_deps.or(self.only_normal_deps),
6062
}
6163
}
6264

cargo-cyclonedx/src/generator.rs

+125-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::config::Describe;
2+
use std::cmp::min;
3+
use std::collections::HashSet;
24
/*
35
* This file is part of CycloneDX Rust Cargo.
46
*
@@ -68,6 +70,35 @@ use validator::validate_email;
6870
// Maps from PackageId to Package for efficiency - faster lookups than in a Vec
6971
type PackageMap = BTreeMap<PackageId, Package>;
7072
type ResolveMap = BTreeMap<PackageId, Node>;
73+
type DependencyKindMap = BTreeMap<PackageId, DependencyKind>;
74+
75+
/// The values are ordered from weakest to strongest so that casting to integer would make sense
76+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
77+
enum PrivateDepKind {
78+
Development,
79+
Build,
80+
Runtime,
81+
}
82+
impl From<PrivateDepKind> for DependencyKind {
83+
fn from(priv_kind: PrivateDepKind) -> Self {
84+
match priv_kind {
85+
PrivateDepKind::Development => DependencyKind::Development,
86+
PrivateDepKind::Build => DependencyKind::Build,
87+
PrivateDepKind::Runtime => DependencyKind::Normal,
88+
}
89+
}
90+
}
91+
92+
impl From<&DependencyKind> for PrivateDepKind {
93+
fn from(kind: &DependencyKind) -> Self {
94+
match kind {
95+
DependencyKind::Normal => PrivateDepKind::Runtime,
96+
DependencyKind::Development => PrivateDepKind::Development,
97+
DependencyKind::Build => PrivateDepKind::Build,
98+
_ => panic!("Unknown dependency kind"),
99+
}
100+
}
101+
}
71102

72103
pub struct SbomGenerator {
73104
config: SbomConfig,
@@ -98,11 +129,13 @@ impl SbomGenerator {
98129
for member in members.iter() {
99130
log::trace!("Processing the package {}", member);
100131

132+
let dep_kinds = index_dep_kinds(member, &resolve);
133+
101134
let (dependencies, pruned_resolve) =
102135
if config.included_dependencies() == IncludedDependencies::AllDependencies {
103-
all_dependencies(member, &packages, &resolve)
136+
all_dependencies(member, &packages, &resolve, config)
104137
} else {
105-
top_level_dependencies(member, &packages, &resolve)
138+
top_level_dependencies(member, &packages, &resolve, config)
106139
};
107140

108141
let manifest_path = packages[member].manifest_path.clone().into_std_path_buf();
@@ -128,7 +161,7 @@ impl SbomGenerator {
128161
crate_hashes,
129162
};
130163
let (bom, target_kinds) =
131-
generator.create_bom(member, &dependencies, &pruned_resolve)?;
164+
generator.create_bom(member, &dependencies, &pruned_resolve, &dep_kinds)?;
132165

133166
let generated = GeneratedSbom {
134167
bom,
@@ -149,14 +182,15 @@ impl SbomGenerator {
149182
package: &PackageId,
150183
packages: &PackageMap,
151184
resolve: &ResolveMap,
185+
dep_kinds: &DependencyKindMap,
152186
) -> Result<(Bom, TargetKinds), GeneratorError> {
153187
let mut bom = Bom::default();
154188
let root_package = &packages[package];
155189

156190
let components: Vec<_> = packages
157191
.values()
158192
.filter(|p| &p.id != package)
159-
.map(|component| self.create_component(component, root_package))
193+
.map(|component| self.create_component(component, root_package, dep_kinds))
160194
.collect();
161195

162196
bom.components = Some(Components(components));
@@ -170,7 +204,12 @@ impl SbomGenerator {
170204
Ok((bom, target_kinds))
171205
}
172206

173-
fn create_component(&self, package: &Package, root_package: &Package) -> Component {
207+
fn create_component(
208+
&self,
209+
package: &Package,
210+
root_package: &Package,
211+
dep_kinds: &DependencyKindMap,
212+
) -> Component {
174213
let name = package.name.to_owned().trim().to_string();
175214
let version = package.version.to_string();
176215

@@ -190,7 +229,13 @@ impl SbomGenerator {
190229
);
191230

192231
component.purl = purl;
193-
component.scope = Some(Scope::Required);
232+
component.scope = match dep_kinds
233+
.get(&package.id)
234+
.unwrap_or(&DependencyKind::Normal)
235+
{
236+
DependencyKind::Normal => Some(Scope::Required),
237+
_ => Some(Scope::Excluded),
238+
};
194239
component.external_references = Self::get_external_references(package);
195240
component.licenses = self.get_licenses(package);
196241
component.hashes = self.get_hashes(package);
@@ -206,7 +251,7 @@ impl SbomGenerator {
206251
/// Same as [Self::create_component] but also includes information
207252
/// on binaries and libraries comprising it as subcomponents
208253
fn create_toplevel_component(&self, package: &Package) -> (Component, TargetKinds) {
209-
let mut top_component = self.create_component(package, package);
254+
let mut top_component = self.create_component(package, package, &DependencyKindMap::new());
210255
let mut subcomponents: Vec<Component> = Vec::new();
211256
let mut target_kinds = HashMap::new();
212257
for tgt in filter_targets(&package.targets) {
@@ -542,6 +587,57 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
542587
.collect()
543588
}
544589

590+
fn index_dep_kinds(root: &PackageId, resolve: &ResolveMap) -> DependencyKindMap {
591+
// cache strongest found dependency kind for every node
592+
let mut id_to_dep_kind: HashMap<PackageId, PrivateDepKind> = HashMap::new();
593+
id_to_dep_kind.insert(root.clone(), PrivateDepKind::Runtime);
594+
595+
type DepNode = (PackageId, PrivateDepKind, PrivateDepKind);
596+
597+
let mut nodes_to_visit: Vec<DepNode> = vec![];
598+
nodes_to_visit.push((
599+
root.clone(),
600+
PrivateDepKind::Runtime,
601+
PrivateDepKind::Runtime,
602+
));
603+
604+
let mut visited_nodes: HashSet<DepNode> = HashSet::new();
605+
606+
// perform a simple iterative DFS over the dependencies,
607+
// mark child deps with the minimum of parent kind and their own strongest value
608+
// therefore e.g. mark decendants of build dependencies as build dependencies,
609+
// as long as they never occur as normal dependency.
610+
while let Some((pkg_id, node_kind, path_node_kind)) = nodes_to_visit.pop() {
611+
visited_nodes.insert((pkg_id.clone(), node_kind, path_node_kind));
612+
613+
let dep_kind_on_previous_visit = id_to_dep_kind.get(&pkg_id);
614+
// insert/update a nodes dependency kind, when its new or stronger than the previous value
615+
if dep_kind_on_previous_visit.is_none()
616+
|| path_node_kind > *dep_kind_on_previous_visit.unwrap()
617+
{
618+
let _ = id_to_dep_kind.insert(pkg_id.clone(), path_node_kind);
619+
}
620+
621+
let node = &resolve[&pkg_id];
622+
for child_dep in &node.deps {
623+
for dep_kind in &child_dep.dep_kinds {
624+
let current_kind = PrivateDepKind::from(&dep_kind.kind);
625+
let new_path_node_kind = min(current_kind, path_node_kind);
626+
627+
let dep_node: DepNode = (child_dep.pkg.clone(), current_kind, new_path_node_kind);
628+
if !visited_nodes.contains(&dep_node) {
629+
nodes_to_visit.push(dep_node);
630+
}
631+
}
632+
}
633+
}
634+
635+
id_to_dep_kind
636+
.iter()
637+
.map(|(x, y)| ((*x).clone(), DependencyKind::from(*y)))
638+
.collect()
639+
}
640+
545641
#[derive(Error, Debug)]
546642
pub enum GeneratorError {
547643
#[error("Expected a root package in the cargo config: {config_filepath}")]
@@ -584,13 +680,15 @@ fn top_level_dependencies(
584680
root: &PackageId,
585681
packages: &PackageMap,
586682
resolve: &ResolveMap,
683+
config: &SbomConfig,
587684
) -> (PackageMap, ResolveMap) {
588685
log::trace!("Adding top-level dependencies to SBOM");
589686

590687
// Only include packages that have dependency kinds other than "Development"
591-
let root_node = strip_dev_dependencies(&resolve[root]);
688+
let root_node = add_filtered_dependencies(&resolve[root], config);
592689

593690
let mut pkg_result = PackageMap::new();
691+
594692
// Record the root package, then its direct non-dev dependencies
595693
pkg_result.insert(root.to_owned(), packages[root].to_owned());
596694
for id in &root_node.dependencies {
@@ -615,6 +713,7 @@ fn all_dependencies(
615713
root: &PackageId,
616714
packages: &PackageMap,
617715
resolve: &ResolveMap,
716+
config: &SbomConfig,
618717
) -> (PackageMap, ResolveMap) {
619718
log::trace!("Adding all dependencies to SBOM");
620719

@@ -635,9 +734,11 @@ fn all_dependencies(
635734
// If we haven't processed this node yet...
636735
if !out_resolve.contains_key(&node.id) {
637736
// Add the node to the output
638-
out_resolve.insert(node.id.to_owned(), strip_dev_dependencies(node));
737+
out_resolve.insert(node.id.to_owned(), add_filtered_dependencies(node, config));
639738
// Queue its dependencies for the next BFS loop iteration
640-
next_queue.extend(non_dev_dependencies(&node.deps).map(|dep| &resolve[&dep.pkg]));
739+
next_queue.extend(
740+
filtered_dependencies(&node.deps, config).map(|dep| &resolve[&dep.pkg]),
741+
);
641742
}
642743
}
643744
std::mem::swap(&mut current_queue, &mut next_queue);
@@ -653,20 +754,27 @@ fn all_dependencies(
653754
(out_packages, out_resolve)
654755
}
655756

656-
fn strip_dev_dependencies(node: &Node) -> Node {
757+
fn add_filtered_dependencies(node: &Node, config: &SbomConfig) -> Node {
657758
let mut node = node.clone();
658-
node.deps = non_dev_dependencies(&node.deps).cloned().collect();
759+
node.deps = filtered_dependencies(&node.deps, config).cloned().collect();
659760
node.dependencies = node.deps.iter().map(|d| d.pkg.to_owned()).collect();
660761
node
661762
}
662763

663764
/// Filters out dependencies only used for development, and not affecting the final binary.
664765
/// These are specified under `[dev-dependencies]` in Cargo.toml.
665-
fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator<Item = &NodeDep> {
766+
fn filtered_dependencies<'a>(
767+
input: &'a [NodeDep],
768+
config: &'a SbomConfig,
769+
) -> impl Iterator<Item = &'a NodeDep> {
666770
input.iter().filter(|p| {
667-
p.dep_kinds
668-
.iter()
669-
.any(|dep| dep.kind != DependencyKind::Development)
771+
p.dep_kinds.iter().any(|dep| {
772+
if let Some(true) = config.only_normal_deps {
773+
dep.kind == DependencyKind::Normal
774+
} else {
775+
dep.kind != DependencyKind::Development
776+
}
777+
})
670778
})
671779
}
672780

@@ -677,6 +785,7 @@ fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator<Item = &NodeDep> {
677785
/// * `package_name` - Package from which this SBOM was generated
678786
/// * `sbom_config` - Configuration options used during generation
679787
/// * `target_kinds` - Detailed information on the kinds of targets in `sbom`
788+
#[derive(Debug)]
680789
pub struct GeneratedSbom {
681790
pub bom: Bom,
682791
pub manifest_path: PathBuf,

0 commit comments

Comments
 (0)