Skip to content

Commit

Permalink
Merge pull request #512 from Shnatsel/feature-selection
Browse files Browse the repository at this point in the history
Add feature selection via CLI arguments
  • Loading branch information
Shnatsel authored Oct 30, 2023
2 parents 62c08e2 + 6c67913 commit 55318a9
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 13 deletions.
89 changes: 88 additions & 1 deletion cargo-cyclonedx/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cargo_cyclonedx::{
config::{
CdxExtension, CustomPrefix, IncludedDependencies, OutputOptions, Pattern, Prefix,
CdxExtension, CustomPrefix, Features, IncludedDependencies, OutputOptions, Pattern, Prefix,
PrefixError, SbomConfig,
},
format::Format,
Expand Down Expand Up @@ -37,6 +37,20 @@ pub struct Args {
#[clap(long = "quiet", short = 'q')]
pub quiet: bool,

// `--all-features`, `--no-default-features` and `--features`
// are not mutually exclusive in Cargo, so we keep the same behavior here too.
/// Activate all available features
#[clap(long = "all-features")]
pub all_features: bool,

/// Do not activate the `default` feature
#[clap(long = "no-default-features")]
pub no_default_features: bool,

/// Space or comma separated list of features to activate
#[clap(long = "features", short = 'F')]
pub features: Vec<String>,

/// List all dependencies instead of only top-level ones
#[clap(long = "all", short = 'a')]
pub all: bool,
Expand Down Expand Up @@ -88,6 +102,32 @@ impl Args {
false => None,
};

let features =
if !self.all_features && !self.no_default_features && self.features.is_empty() {
None
} else {
let mut feature_list: Vec<String> = Vec::new();
// Features can be comma- or space-separated for compatibility with Cargo,
// but only in command-line arguments (not in config files),
// which is why this code lives here.
for comma_separated_features in &self.features {
// Feature names themselves never contain commas.
for space_separated_features in comma_separated_features.split(',') {
for feature in space_separated_features.split(' ') {
if !feature.is_empty() {
feature_list.push(feature.to_owned());
}
}
}
}

Some(Features {
all_features: self.all_features,
no_default_features: self.no_default_features,
features: feature_list,
})
};

let output_options = match (cdx_extension, prefix) {
(Some(cdx_extension), Some(prefix)) => Some(OutputOptions {
cdx_extension,
Expand All @@ -108,6 +148,7 @@ impl Args {
format: self.format,
included_dependencies,
output_options,
features,
})
}
}
Expand All @@ -117,3 +158,49 @@ pub enum ArgsError {
#[error("Invalid prefix from CLI")]
CustomPrefixError(#[from] PrefixError),
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_features() {
let args = vec!["cyclonedx"];
let config = parse_to_config(&args);
assert!(config.features.is_none());

let args = vec!["cyclonedx", "--features=foo"];
let config = parse_to_config(&args);
assert!(contains_feature(&config, "foo"));

let args = vec!["cyclonedx", "--features=foo", "--features=bar"];
let config = parse_to_config(&args);
assert!(contains_feature(&config, "foo"));
assert!(contains_feature(&config, "bar"));

let args = vec!["cyclonedx", "--features=foo,bar baz"];
let config = parse_to_config(&args);
assert!(contains_feature(&config, "foo"));
assert!(contains_feature(&config, "bar"));
assert!(contains_feature(&config, "baz"));

let args = vec!["cyclonedx", "--features=foo, bar"];
let config = parse_to_config(&args);
assert!(contains_feature(&config, "foo"));
assert!(contains_feature(&config, "bar"));
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
.as_ref()
.unwrap()
.features
.contains(&feature.to_owned())
}
}
17 changes: 11 additions & 6 deletions cargo-cyclonedx/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,17 @@ use thiserror::Error;
*/
use crate::format::Format;

#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SbomConfig {
pub format: Option<Format>,
pub included_dependencies: Option<IncludedDependencies>,
pub output_options: Option<OutputOptions>,
pub features: Option<Features>,
}

impl SbomConfig {
pub fn empty_config() -> Self {
Self {
format: None,
included_dependencies: None,
output_options: None,
}
Default::default()
}

pub fn merge(&self, other: &SbomConfig) -> SbomConfig {
Expand All @@ -45,6 +42,7 @@ impl SbomConfig {
.output_options
.clone()
.or_else(|| self.output_options.clone()),
features: other.features.clone().or_else(|| self.features.clone()),
}
}

Expand Down Expand Up @@ -121,6 +119,13 @@ impl Default for CdxExtension {
}
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Features {
pub all_features: bool,
pub no_default_features: bool,
pub features: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Prefix {
Pattern(Pattern),
Expand Down
30 changes: 24 additions & 6 deletions cargo-cyclonedx/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use cargo_cyclonedx::generator::SbomGenerator;
use cargo_cyclonedx::{config::SbomConfig, generator::SbomGenerator};
use std::{
io::{self},
path::{Path, PathBuf},
};

use cargo_metadata::{self, Metadata};
use cargo_metadata::{self, CargoOpt, Metadata};

use anyhow::Result;
use clap::Parser;
Expand All @@ -70,7 +70,7 @@ fn main() -> anyhow::Result<()> {
log::debug!("Found the Cargo.toml file at {}", manifest_path.display());

log::trace!("Running `cargo metadata` started");
let metadata = get_metadata(&args, &manifest_path)?;
let metadata = get_metadata(&args, &manifest_path, &cli_config)?;
log::trace!("Running `cargo metadata` finished");

log::trace!("SBOM generation started");
Expand Down Expand Up @@ -128,9 +128,27 @@ fn locate_manifest(args: &Args) -> Result<PathBuf, io::Error> {
}
}

fn get_metadata(_args: &Args, manifest_path: &Path) -> anyhow::Result<Metadata> {
fn get_metadata(
_args: &Args,
manifest_path: &Path,
config: &SbomConfig,
) -> anyhow::Result<Metadata> {
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.manifest_path(manifest_path);
// TODO: allow customizing the target platform, etc.
cmd.exec().map_err(|e| e.into())

if let Some(feature_configuration) = config.features.as_ref() {
if feature_configuration.all_features {
cmd.features(CargoOpt::AllFeatures);
}
if feature_configuration.no_default_features {
cmd.features(CargoOpt::NoDefaultFeatures);
}
if !feature_configuration.features.is_empty() {
cmd.features(CargoOpt::SomeFeatures(
feature_configuration.features.clone(),
));
}
}

Ok(cmd.exec()?)
}
1 change: 1 addition & 0 deletions cargo-cyclonedx/src/toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl TryFrom<TomlConfig> for SbomConfig {
format: value.format,
included_dependencies: value.included_dependencies.map(Into::into),
output_options,
features: None, // Not possible to support on per-Cargo.toml basis
})
}
}
Expand Down

0 comments on commit 55318a9

Please sign in to comment.