Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature selection via CLI arguments #512

Merged
merged 13 commits into from
Oct 30, 2023
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes need to be reflected in the README and it should probably also mention (even though it can be inferred) that by default all default features are enabled (at least I assume that's the case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've copied the help messages from Cargo itself here.

I'll update the README before release, because otherwise I'll run into lots of merge conflicts. That text is not neatly split into lines, unlike code.


/// 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 {
Shnatsel marked this conversation as resolved.
Show resolved Hide resolved
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