diff --git a/cargo-cyclonedx/src/cli.rs b/cargo-cyclonedx/src/cli.rs index 879ba34c..e3f2f9bc 100644 --- a/cargo-cyclonedx/src/cli.rs +++ b/cargo-cyclonedx/src/cli.rs @@ -100,6 +100,10 @@ Defaults to the host target, as printed by 'rustc -vV'" /// The CycloneDX specification version to output: `1.3`, `1.4` or `1.5`. Defaults to 1.3 #[clap(long = "spec-version")] pub spec_version: Option, + + /// List only dependencies of kind normal (no build deps, no dev deps) + #[clap(name = "no-build-deps", long = "no-build-deps")] + pub no_build_deps: bool, } impl Args { @@ -170,6 +174,7 @@ impl Args { let describe = self.describe; let spec_version = self.spec_version; + let only_normal_deps = Some(self.no_build_deps); Ok(SbomConfig { format: self.format, @@ -180,6 +185,7 @@ impl Args { license_parser, describe, spec_version, + only_normal_deps, }) } } @@ -190,6 +196,11 @@ pub enum ArgsError { FilenameOverrideError(#[from] FilenameOverrideError), } +#[cfg(test)] +pub fn parse_to_config(args: &[&str]) -> SbomConfig { + Args::parse_from(args.iter()).as_config().unwrap() +} + #[cfg(test)] mod tests { use super::*; @@ -222,10 +233,6 @@ mod tests { assert!(!contains_feature(&config, "")); } - fn parse_to_config(args: &[&str]) -> SbomConfig { - Args::parse_from(args.iter()).as_config().unwrap() - } - fn contains_feature(config: &SbomConfig, feature: &str) -> bool { config .features diff --git a/cargo-cyclonedx/src/config.rs b/cargo-cyclonedx/src/config.rs index c3a11d88..2c54bd5c 100644 --- a/cargo-cyclonedx/src/config.rs +++ b/cargo-cyclonedx/src/config.rs @@ -33,6 +33,7 @@ pub struct SbomConfig { pub license_parser: Option, pub describe: Option, pub spec_version: Option, + pub only_normal_deps: Option, } impl SbomConfig { @@ -57,6 +58,7 @@ impl SbomConfig { .or_else(|| self.license_parser.clone()), describe: other.describe.or(self.describe), spec_version: other.spec_version.or(self.spec_version), + only_normal_deps: other.only_normal_deps.or(self.only_normal_deps), } } diff --git a/cargo-cyclonedx/src/generator.rs b/cargo-cyclonedx/src/generator.rs index 0f453597..b758cf0d 100644 --- a/cargo-cyclonedx/src/generator.rs +++ b/cargo-cyclonedx/src/generator.rs @@ -1,4 +1,6 @@ use crate::config::Describe; +use std::cmp::min; +use std::collections::HashSet; /* * This file is part of CycloneDX Rust Cargo. * @@ -68,6 +70,35 @@ use validator::validate_email; // Maps from PackageId to Package for efficiency - faster lookups than in a Vec type PackageMap = BTreeMap; type ResolveMap = BTreeMap; +type DependencyKindMap = BTreeMap; + +/// The values are ordered from weakest to strongest so that casting to integer would make sense +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +enum PrivateDepKind { + Development, + Build, + Runtime, +} +impl From for DependencyKind { + fn from(priv_kind: PrivateDepKind) -> Self { + match priv_kind { + PrivateDepKind::Development => DependencyKind::Development, + PrivateDepKind::Build => DependencyKind::Build, + PrivateDepKind::Runtime => DependencyKind::Normal, + } + } +} + +impl From<&DependencyKind> for PrivateDepKind { + fn from(kind: &DependencyKind) -> Self { + match kind { + DependencyKind::Normal => PrivateDepKind::Runtime, + DependencyKind::Development => PrivateDepKind::Development, + DependencyKind::Build => PrivateDepKind::Build, + _ => panic!("Unknown dependency kind"), + } + } +} pub struct SbomGenerator { config: SbomConfig, @@ -98,11 +129,13 @@ impl SbomGenerator { for member in members.iter() { log::trace!("Processing the package {}", member); + let dep_kinds = index_dep_kinds(member, &resolve); + let (dependencies, pruned_resolve) = if config.included_dependencies() == IncludedDependencies::AllDependencies { - all_dependencies(member, &packages, &resolve) + all_dependencies(member, &packages, &resolve, config) } else { - top_level_dependencies(member, &packages, &resolve) + top_level_dependencies(member, &packages, &resolve, config) }; let manifest_path = packages[member].manifest_path.clone().into_std_path_buf(); @@ -128,7 +161,7 @@ impl SbomGenerator { crate_hashes, }; let (bom, target_kinds) = - generator.create_bom(member, &dependencies, &pruned_resolve)?; + generator.create_bom(member, &dependencies, &pruned_resolve, &dep_kinds)?; let generated = GeneratedSbom { bom, @@ -149,6 +182,7 @@ impl SbomGenerator { package: &PackageId, packages: &PackageMap, resolve: &ResolveMap, + dep_kinds: &DependencyKindMap, ) -> Result<(Bom, TargetKinds), GeneratorError> { let mut bom = Bom::default(); let root_package = &packages[package]; @@ -156,7 +190,7 @@ impl SbomGenerator { let components: Vec<_> = packages .values() .filter(|p| &p.id != package) - .map(|component| self.create_component(component, root_package)) + .map(|component| self.create_component(component, root_package, dep_kinds)) .collect(); bom.components = Some(Components(components)); @@ -170,7 +204,12 @@ impl SbomGenerator { Ok((bom, target_kinds)) } - fn create_component(&self, package: &Package, root_package: &Package) -> Component { + fn create_component( + &self, + package: &Package, + root_package: &Package, + dep_kinds: &DependencyKindMap, + ) -> Component { let name = package.name.to_owned().trim().to_string(); let version = package.version.to_string(); @@ -190,7 +229,13 @@ impl SbomGenerator { ); component.purl = purl; - component.scope = Some(Scope::Required); + component.scope = match dep_kinds + .get(&package.id) + .unwrap_or(&DependencyKind::Normal) + { + DependencyKind::Normal => Some(Scope::Required), + _ => Some(Scope::Excluded), + }; component.external_references = Self::get_external_references(package); component.licenses = self.get_licenses(package); component.hashes = self.get_hashes(package); @@ -206,7 +251,7 @@ impl SbomGenerator { /// Same as [Self::create_component] but also includes information /// on binaries and libraries comprising it as subcomponents fn create_toplevel_component(&self, package: &Package) -> (Component, TargetKinds) { - let mut top_component = self.create_component(package, package); + let mut top_component = self.create_component(package, package, &DependencyKindMap::new()); let mut subcomponents: Vec = Vec::new(); let mut target_kinds = HashMap::new(); for tgt in filter_targets(&package.targets) { @@ -542,6 +587,57 @@ fn index_resolve(packages: Vec) -> ResolveMap { .collect() } +fn index_dep_kinds(root: &PackageId, resolve: &ResolveMap) -> DependencyKindMap { + // cache strongest found dependency kind for every node + let mut id_to_dep_kind: HashMap = HashMap::new(); + id_to_dep_kind.insert(root.clone(), PrivateDepKind::Runtime); + + type DepNode = (PackageId, PrivateDepKind, PrivateDepKind); + + let mut nodes_to_visit: Vec = vec![]; + nodes_to_visit.push(( + root.clone(), + PrivateDepKind::Runtime, + PrivateDepKind::Runtime, + )); + + let mut visited_nodes: HashSet = HashSet::new(); + + // perform a simple iterative DFS over the dependencies, + // mark child deps with the minimum of parent kind and their own strongest value + // therefore e.g. mark decendants of build dependencies as build dependencies, + // as long as they never occur as normal dependency. + while let Some((pkg_id, node_kind, path_node_kind)) = nodes_to_visit.pop() { + visited_nodes.insert((pkg_id.clone(), node_kind, path_node_kind)); + + let dep_kind_on_previous_visit = id_to_dep_kind.get(&pkg_id); + // insert/update a nodes dependency kind, when its new or stronger than the previous value + if dep_kind_on_previous_visit.is_none() + || path_node_kind > *dep_kind_on_previous_visit.unwrap() + { + let _ = id_to_dep_kind.insert(pkg_id.clone(), path_node_kind); + } + + let node = &resolve[&pkg_id]; + for child_dep in &node.deps { + for dep_kind in &child_dep.dep_kinds { + let current_kind = PrivateDepKind::from(&dep_kind.kind); + let new_path_node_kind = min(current_kind, path_node_kind); + + let dep_node: DepNode = (child_dep.pkg.clone(), current_kind, new_path_node_kind); + if !visited_nodes.contains(&dep_node) { + nodes_to_visit.push(dep_node); + } + } + } + } + + id_to_dep_kind + .iter() + .map(|(x, y)| ((*x).clone(), DependencyKind::from(*y))) + .collect() +} + #[derive(Error, Debug)] pub enum GeneratorError { #[error("Expected a root package in the cargo config: {config_filepath}")] @@ -584,13 +680,15 @@ fn top_level_dependencies( root: &PackageId, packages: &PackageMap, resolve: &ResolveMap, + config: &SbomConfig, ) -> (PackageMap, ResolveMap) { log::trace!("Adding top-level dependencies to SBOM"); // Only include packages that have dependency kinds other than "Development" - let root_node = strip_dev_dependencies(&resolve[root]); + let root_node = add_filtered_dependencies(&resolve[root], config); let mut pkg_result = PackageMap::new(); + // Record the root package, then its direct non-dev dependencies pkg_result.insert(root.to_owned(), packages[root].to_owned()); for id in &root_node.dependencies { @@ -615,6 +713,7 @@ fn all_dependencies( root: &PackageId, packages: &PackageMap, resolve: &ResolveMap, + config: &SbomConfig, ) -> (PackageMap, ResolveMap) { log::trace!("Adding all dependencies to SBOM"); @@ -635,9 +734,11 @@ fn all_dependencies( // If we haven't processed this node yet... if !out_resolve.contains_key(&node.id) { // Add the node to the output - out_resolve.insert(node.id.to_owned(), strip_dev_dependencies(node)); + out_resolve.insert(node.id.to_owned(), add_filtered_dependencies(node, config)); // Queue its dependencies for the next BFS loop iteration - next_queue.extend(non_dev_dependencies(&node.deps).map(|dep| &resolve[&dep.pkg])); + next_queue.extend( + filtered_dependencies(&node.deps, config).map(|dep| &resolve[&dep.pkg]), + ); } } std::mem::swap(&mut current_queue, &mut next_queue); @@ -653,20 +754,27 @@ fn all_dependencies( (out_packages, out_resolve) } -fn strip_dev_dependencies(node: &Node) -> Node { +fn add_filtered_dependencies(node: &Node, config: &SbomConfig) -> Node { let mut node = node.clone(); - node.deps = non_dev_dependencies(&node.deps).cloned().collect(); + node.deps = filtered_dependencies(&node.deps, config).cloned().collect(); node.dependencies = node.deps.iter().map(|d| d.pkg.to_owned()).collect(); node } /// Filters out dependencies only used for development, and not affecting the final binary. /// These are specified under `[dev-dependencies]` in Cargo.toml. -fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator { +fn filtered_dependencies<'a>( + input: &'a [NodeDep], + config: &'a SbomConfig, +) -> impl Iterator { input.iter().filter(|p| { - p.dep_kinds - .iter() - .any(|dep| dep.kind != DependencyKind::Development) + p.dep_kinds.iter().any(|dep| { + if let Some(true) = config.only_normal_deps { + dep.kind == DependencyKind::Normal + } else { + dep.kind != DependencyKind::Development + } + }) }) } @@ -677,6 +785,7 @@ fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator { /// * `package_name` - Package from which this SBOM was generated /// * `sbom_config` - Configuration options used during generation /// * `target_kinds` - Detailed information on the kinds of targets in `sbom` +#[derive(Debug)] pub struct GeneratedSbom { pub bom: Bom, pub manifest_path: PathBuf, diff --git a/cargo-cyclonedx/src/main.rs b/cargo-cyclonedx/src/main.rs index 51a87e4a..103c2ab7 100644 --- a/cargo-cyclonedx/src/main.rs +++ b/cargo-cyclonedx/src/main.rs @@ -48,6 +48,7 @@ use cargo_cyclonedx::{ config::{SbomConfig, Target}, generator::SbomGenerator, + GeneratedSbom, }; use std::{ @@ -65,22 +66,28 @@ use log::LevelFilter; mod cli; use cli::{Args, Opts}; -fn main() -> anyhow::Result<()> { - let Opts::Bom(args) = Opts::parse(); - setup_logging(&args)?; - +fn generate_sboms(args: &Args) -> Result> { let cli_config = args.as_config()?; - let manifest_path = locate_manifest(&args)?; + let manifest_path = locate_manifest(args)?; log::debug!("Found the Cargo.toml file at {}", manifest_path.display()); log::trace!("Running `cargo metadata` started"); - let metadata = get_metadata(&args, &manifest_path, &cli_config)?; + let metadata = get_metadata(args, &manifest_path, &cli_config)?; log::trace!("Running `cargo metadata` finished"); log::trace!("SBOM generation started"); let boms = SbomGenerator::create_sboms(metadata, &cli_config)?; log::trace!("SBOM generation finished"); + Ok(boms) +} + +fn main() -> anyhow::Result<()> { + let Opts::Bom(args) = Opts::parse(); + setup_logging(&args)?; + + let boms = generate_sboms(&args)?; + log::trace!("SBOM output started"); for bom in boms { bom.write_to_files()?; @@ -163,3 +170,62 @@ fn get_metadata( Ok(cmd.exec()?) } + +#[cfg(test)] +mod tests { + use cyclonedx_bom::prelude::NormalizedString; + + #[test] + fn parse_toml_only_normal() { + use crate::cli; + use crate::generate_sboms; + use clap::Parser; + use cyclonedx_bom::models::component::Scope; + use std::path::PathBuf; + + let mut test_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + test_cargo_toml.push("tests/fixtures/build_then_runtime_dep/Cargo.toml"); + + let path_arg = &format!("--manifest-path={}", test_cargo_toml.display()); + let args = ["cyclonedx", path_arg, "--no-build-deps"]; + let args_parsed = cli::Args::parse_from(args.iter()); + + let sboms = generate_sboms(&args_parsed).unwrap(); + + let components = sboms[0].bom.components.as_ref().unwrap(); + assert!(components + .0 + .iter() + .all(|f| f.scope == Some(Scope::Required))); + } + + #[test] + fn parse_toml_with_excluded() { + use crate::cli; + use crate::generate_sboms; + use clap::Parser; + use cyclonedx_bom::models::component::Scope; + use std::path::PathBuf; + + let mut test_cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + test_cargo_toml.push("tests/fixtures/build_then_runtime_dep/Cargo.toml"); + + let path_arg = &format!("--manifest-path={}", test_cargo_toml.display()); + let args = ["cyclonedx", path_arg]; + let args_parsed = cli::Args::parse_from(args.iter()); + + let sboms = generate_sboms(&args_parsed).unwrap(); + + // build_dep is a build dependency -> excluded + // runtime_dep_of_build_dep is a dependency of a build dependency -> excluded + let components = sboms[0].bom.components.as_ref().unwrap(); + assert!(components + .0 + .iter() + .all(|c| c.name != NormalizedString::new("build_dep") + || c.scope == Some(Scope::Excluded))); + assert!(components.0.iter().all(|c| c.name + != NormalizedString::new("runtime_dep_of_build_dep") + || c.scope == Some(Scope::Excluded))); + } +} diff --git a/cargo-cyclonedx/src/purl.rs b/cargo-cyclonedx/src/purl.rs index 10da4d92..4d600354 100644 --- a/cargo-cyclonedx/src/purl.rs +++ b/cargo-cyclonedx/src/purl.rs @@ -105,7 +105,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "aho-corasick"); assert_eq!(parsed_purl.version(), Some("1.1.2")); assert!(parsed_purl.qualifiers().is_empty()); @@ -118,7 +118,7 @@ mod tests { let git_package: Package = serde_json::from_str(GIT_PACKAGE_JSON).unwrap(); let purl = get_purl(&git_package, &git_package, Utf8Path::new("/foo/bar"), None).unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "auditable-extract"); assert_eq!(parsed_purl.version(), Some("0.3.2")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -140,7 +140,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "cargo-cyclonedx"); assert_eq!(parsed_purl.version(), Some("0.3.8")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -163,7 +163,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "cargo-cyclonedx"); assert_eq!(parsed_purl.version(), Some("0.3.8")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -187,7 +187,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "cyclonedx-bom"); assert_eq!(parsed_purl.version(), Some("0.4.1")); assert_eq!(parsed_purl.qualifiers().len(), 1); @@ -211,7 +211,7 @@ mod tests { ) .unwrap(); // Validate that data roundtripped correctly - let parsed_purl = Purl::from_str(&purl.to_string()).unwrap(); + let parsed_purl = Purl::from_str(purl.as_ref()).unwrap(); assert_eq!(parsed_purl.name(), "cyclonedx-bom"); assert_eq!(parsed_purl.version(), Some("0.4.1")); assert_eq!(parsed_purl.qualifiers().len(), 1); diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.lock b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.lock new file mode 100644 index 00000000..1a96d8f9 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.lock @@ -0,0 +1,31 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "build_dep" +version = "0.1.0" +dependencies = [ + "runtime_dep_of_build_dep", +] + +[[package]] +name = "runtime_dep_of_build_dep" +version = "0.1.0" +dependencies = [ + "runtime_dep_of_runtime_dep", +] + +[[package]] +name = "runtime_dep_of_runtime_dep" +version = "0.1.0" +dependencies = [ + "build_dep", +] + +[[package]] +name = "top_level_crate" +version = "0.1.0" +dependencies = [ + "build_dep", +] diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml new file mode 100644 index 00000000..a1531f28 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "top_level_crate", + "build_dep", + "runtime_dep_of_build_dep", +] diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml new file mode 100644 index 00000000..2d1958f8 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "build_dep" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +runtime_dep_of_build_dep = {path = "../runtime_dep_of_build_dep"} \ No newline at end of file diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs new file mode 100644 index 00000000..1b4a90c9 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/build_dep/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml new file mode 100644 index 00000000..0376fdcf --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "runtime_dep_of_build_dep" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +runtime_dep_of_runtime_dep = {path = "../runtime_dep_of_runtime_dep"} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs new file mode 100644 index 00000000..1b4a90c9 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_build_dep/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/Cargo.toml new file mode 100644 index 00000000..90a4e046 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "runtime_dep_of_runtime_dep" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dev-dependencies] +build_dep = {path = "../build_dep"} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/src/lib.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/src/lib.rs new file mode 100644 index 00000000..1b4a90c9 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/runtime_dep_of_runtime_dep/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml new file mode 100644 index 00000000..f2373ea9 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "top_level_crate" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +build_dep = {path = "../build_dep"} \ No newline at end of file diff --git a/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/cargo-cyclonedx/tests/fixtures/build_then_runtime_dep/top_level_crate/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}