diff --git a/.gitignore b/.gitignore index 53eaa21..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target -**/*.rs.bk +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index c274764..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,89 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "cargo-subcommand" -version = "0.7.0" -dependencies = [ - "dunce", - "glob", - "serde", - "toml", -] - -[[package]] -name = "dunce" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "proc-macro2" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "serde" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "1.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/Cargo.toml b/Cargo.toml index 3f3b9f2..ca84742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,11 @@ description = "Library for creating cargo subcommands." repository = "https://github.com/dvc94ch/cargo-subcommand" license = "ISC" +[features] +default = ["clap"] + [dependencies] +clap = { version = "3.1.6", optional = true, features = ["derive"] } dunce = "1.0.1" glob = "0.3.0" serde = { version = "1.0.123", features = ["derive"] } diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..ce8da38 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,133 @@ +use crate::profile::Profile; +#[cfg(feature = "clap")] +use clap::Parser; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "clap", derive(Parser))] +pub struct Args { + /// No output printed to stdout + #[cfg_attr(feature = "clap", clap(long, short))] + pub quiet: bool, + + /// Package to build + #[cfg_attr(feature = "clap", clap(long, short))] + pub package: Vec, + /// Build all packages in the workspace + #[cfg_attr(feature = "clap", clap(long))] + pub workspace: bool, + /// Exclude packages from the build + #[cfg_attr(feature = "clap", clap(long))] + pub exclude: Vec, + + /// Build only this package's library + #[cfg_attr(feature = "clap", clap(long))] + pub lib: bool, + /// Build only the specified binary + #[cfg_attr(feature = "clap", clap(long))] + pub bin: Vec, + /// Build all binaries + #[cfg_attr(feature = "clap", clap(long, conflicts_with = "bin"))] + pub bins: bool, + /// Build only the specified example + #[cfg_attr(feature = "clap", clap(long))] + pub example: Vec, + /// Build all examples + #[cfg_attr(feature = "clap", clap(long, conflicts_with = "example"))] + pub examples: bool, + + /// Build artifacts in release mode, with optimizations + #[cfg_attr(feature = "clap", clap(long))] + pub release: bool, + /// Build artifacts with the specified profile + #[cfg_attr(feature = "clap", clap(long, conflicts_with = "release"))] + pub profile: Option, + /// Space or comma separated list of features to activate + #[cfg_attr(feature = "clap", clap(long))] + pub features: Vec, + /// Activate all available features + #[cfg_attr(feature = "clap", clap(long))] + pub all_features: bool, + /// Do not activate the `default` feature + #[cfg_attr(feature = "clap", clap(long))] + pub no_default_features: bool, + /// Build for the target triple + #[cfg_attr(feature = "clap", clap(long))] + pub target: Option, + /// Directory for all generated artifacts + #[cfg_attr(feature = "clap", clap(long))] + pub target_dir: Option, + /// Path to Cargo.toml + #[cfg_attr(feature = "clap", clap(long))] + pub manifest_path: Option, +} + +impl Args { + pub fn apply(&self, cmd: &mut Command) { + if self.quiet { + cmd.arg("--quiet"); + } + + for package in &self.package { + cmd.arg("--package").arg(package); + } + if self.workspace { + cmd.arg("--workspace"); + } + for exclude in &self.exclude { + cmd.arg("--exclude").arg(exclude); + } + + if self.lib { + cmd.arg("--lib"); + } + for bin in &self.bin { + cmd.arg("--bin").arg(bin); + } + if self.bins { + cmd.arg("--bins"); + } + for example in &self.example { + cmd.arg("--example").arg(example); + } + if self.examples { + cmd.arg("--examples"); + } + + if self.release { + cmd.arg("--release"); + } + if let Some(profile) = self.profile.as_ref() { + cmd.arg("--profile").arg(profile.to_string()); + } + for features in &self.features { + cmd.arg("--features").arg(features); + } + if self.all_features { + cmd.arg("--all-features"); + } + if self.no_default_features { + cmd.arg("--no-default-features"); + } + if let Some(target) = self.target.as_ref() { + cmd.arg("--target").arg(target); + } + if let Some(target_dir) = self.target_dir.as_ref() { + cmd.arg("--target-dir").arg(target_dir); + } + if let Some(manifest_path) = self.manifest_path.as_ref() { + cmd.arg("--manifest-path").arg(manifest_path); + } + } + + pub fn profile(&self) -> Profile { + if let Some(profile) = self.profile.as_ref() { + profile.clone() + } else if self.release { + Profile::Release + } else { + Profile::Dev + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8d9ff96..b4f0681 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod args; mod artifact; mod config; mod error; @@ -6,6 +7,7 @@ mod profile; mod subcommand; mod utils; +pub use args::Args; pub use artifact::{Artifact, CrateType}; pub use config::{EnvError, EnvOption, LocalizedConfig}; pub use error::Error; diff --git a/src/main.rs b/src/main.rs index 7c3e84e..378d662 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ -use cargo_subcommand::Subcommand; +use cargo_subcommand::{Args, Subcommand}; +use clap::Parser; fn main() { - let cmd = Subcommand::new(std::env::args(), "subcommand", |_, _| Ok(false)).unwrap(); + let args = Args::parse(); + let cmd = Subcommand::new(args).unwrap(); println!("{:#?}", cmd); } diff --git a/src/profile.rs b/src/profile.rs index 10a4f97..bf9cf97 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,3 +1,4 @@ +use crate::error::Error; use std::path::Path; #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -7,6 +8,28 @@ pub enum Profile { Custom(String), } +impl std::fmt::Display for Profile { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(match self { + Self::Dev => "dev", + Self::Release => "release", + Self::Custom(custom) => custom, + }) + } +} + +impl std::str::FromStr for Profile { + type Err = Error; + + fn from_str(profile: &str) -> Result { + Ok(match profile { + "dev" => Profile::Dev, + "release" => Profile::Release, + custom => Profile::Custom(custom.into()), + }) + } +} + impl AsRef for Profile { fn as_ref(&self) -> &Path { Path::new(match self { diff --git a/src/subcommand.rs b/src/subcommand.rs index a25091e..249fe7b 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,4 +1,5 @@ -use crate::artifact::Artifact; +use crate::args::Args; +use crate::artifact::{Artifact, CrateType}; use crate::error::Error; use crate::profile::Profile; use crate::{utils, LocalizedConfig}; @@ -8,89 +9,43 @@ use std::process::Command; #[derive(Debug)] pub struct Subcommand { - cmd: String, - args: Vec, + args: Args, package: String, manifest: PathBuf, target_dir: PathBuf, - target: Option, host_triple: String, profile: Profile, artifacts: Vec, - quiet: bool, config: Option, } impl Subcommand { - pub fn new) -> Result>( - args: impl Iterator, - subcommand: &'static str, - mut parser: F, - ) -> Result { - let mut args = args.peekable(); - args.next().ok_or(Error::InvalidArgs)?; - let arg = args.next().ok_or(Error::InvalidArgs)?; - if arg != subcommand { - return Err(Error::InvalidArgs); - } - let cmd = args.next().unwrap_or_else(|| "--help".to_string()); - let mut cargo_args = Vec::new(); - let mut target = None; - let mut profile = Profile::Dev; - let mut artifacts = Vec::new(); - let mut target_dir = None; - let mut manifest_path = None; - let mut package = None; - let mut examples = false; - let mut bins = false; - let mut quiet = false; - while let Some(mut name) = args.next() { - let value = if let Some(position) = name.as_str().find('=') { - name.remove(position); // drop the '=' sign so we can cleanly split the string in two - Some(name.split_off(position)) - } else if let Some(value) = args.peek() { - if !value.starts_with('-') { - args.next() - } else { - None - } - } else { - None - }; - let value_ref = value.as_deref(); - let mut matched = true; - match (name.as_str(), value_ref) { - ("--quiet", None) => quiet = true, - ("--release", None) => profile = Profile::Release, - ("--target", Some(value)) => target = Some(value.to_string()), - ("--profile", Some("dev")) => profile = Profile::Dev, - ("--profile", Some("release")) => profile = Profile::Release, - ("--profile", Some(value)) => profile = Profile::Custom(value.to_string()), - ("--example", Some(value)) => artifacts.push(Artifact::Example(value.to_string())), - ("--examples", None) => examples = true, - ("--bin", Some(value)) => artifacts.push(Artifact::Root(value.to_string())), - ("--bins", None) => bins = true, - ("--package", Some(value)) | ("-p", Some(value)) => { - package = Some(value.to_string()) - } - ("--target-dir", Some(value)) => target_dir = Some(PathBuf::from(value)), - ("--manifest-path", Some(value)) => manifest_path = Some(PathBuf::from(value)), - _ => matched = false, - } - if matched || !parser(name.as_str(), value_ref)? { - cargo_args.push(name); - if let Some(value) = value { - cargo_args.push(value); - } - } - } - let (manifest, package) = utils::find_package( - &manifest_path.unwrap_or_else(|| std::env::current_dir().unwrap()), - package.as_deref(), + pub fn new(args: Args) -> Result { + // TODO: support multiple packages properly + assert!( + args.package.len() < 2, + "Multiple packages are not supported yet by `cargo-subcommand`" + ); + assert!( + !args.workspace, + "`--workspace` is not supported yet by `cargo-subcommand`" + ); + assert!( + args.exclude.is_empty(), + "`--exclude` is not supported yet by `cargo-subcommand`" + ); + let (manifest_path, package) = utils::find_package( + &args + .manifest_path + .clone() + .unwrap_or_else(|| std::env::current_dir().unwrap()), + args.package.get(0).map(|s| s.as_str()), )?; - let root_dir = manifest.parent().unwrap(); + let root_dir = manifest_path.parent().unwrap(); - let target_dir = target_dir + let target_dir = args + .target_dir + .clone() .or_else(|| { std::env::var_os("CARGO_BUILD_TARGET_DIR") .or_else(|| std::env::var_os("CARGO_TARGET_DIR")) @@ -112,22 +67,32 @@ impl Subcommand { } let target_dir = target_dir.unwrap_or_else(|| { - utils::find_workspace(&manifest, &package) + utils::find_workspace(&manifest_path, &package) .unwrap() - .unwrap_or_else(|| manifest.clone()) + .unwrap_or_else(|| manifest_path.clone()) .parent() .unwrap() .join(utils::get_target_dir_name(config.as_deref()).unwrap()) }); - if examples { + + let mut artifacts = vec![]; + if args.examples { for file in utils::list_rust_files(&root_dir.join("examples"))? { artifacts.push(Artifact::Example(file)); } + } else { + for example in &args.example { + artifacts.push(Artifact::Example(example.into())); + } } - if bins { + if args.bins { for file in utils::list_rust_files(&root_dir.join("src").join("bin"))? { artifacts.push(Artifact::Root(file)); } + } else { + for bin in &args.bin { + artifacts.push(Artifact::Root(bin.into())); + } } if artifacts.is_empty() { artifacts.push(Artifact::Root(package.clone())); @@ -142,26 +107,20 @@ impl Subcommand { .find(|l| l.starts_with("host: ")) .map(|l| l[6..].to_string()) .ok_or(Error::RustcNotFound)?; + let profile = args.profile(); Ok(Self { - cmd, - args: cargo_args, + args, package, - manifest, + manifest: manifest_path, target_dir, - target, host_triple, profile, artifacts, - quiet, config, }) } - pub fn cmd(&self) -> &str { - &self.cmd - } - - pub fn args(&self) -> &[String] { + pub fn args(&self) -> &Args { &self.args } @@ -174,7 +133,7 @@ impl Subcommand { } pub fn target(&self) -> Option<&str> { - self.target.as_deref() + self.args.target.as_deref() } pub fn profile(&self) -> &Profile { @@ -194,44 +153,31 @@ impl Subcommand { } pub fn quiet(&self) -> bool { - self.quiet + self.args.quiet } pub fn config(&self) -> Option<&LocalizedConfig> { self.config.as_ref() } -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_separator_space() { - let args = [ - "cargo", - "subcommand", - "build", - "--target", - "x86_64-unknown-linux-gnu", - ] - .iter() - .map(|s| s.to_string()); - let cmd = Subcommand::new(args, "subcommand", |_, _| Ok(false)).unwrap(); - assert_eq!(cmd.target(), Some("x86_64-unknown-linux-gnu")); + pub fn build_dir(&self, target: Option<&str>) -> PathBuf { + let target_dir = dunce::simplified(self.target_dir()); + let arch_dir = if let Some(target) = target { + target_dir.join(target) + } else { + target_dir.to_path_buf() + }; + arch_dir.join(self.profile()) } - #[test] - fn test_separator_equals() { - let args = [ - "cargo", - "subcommand", - "build", - "--target=x86_64-unknown-linux-gnu", - ] - .iter() - .map(|s| s.to_string()); - let cmd = Subcommand::new(args, "subcommand", |_, _| Ok(false)).unwrap(); - assert_eq!(cmd.target(), Some("x86_64-unknown-linux-gnu")); + pub fn artifact( + &self, + artifact: &Artifact, + target: Option<&str>, + crate_type: CrateType, + ) -> PathBuf { + let triple = target.unwrap_or_else(|| self.host_triple()); + let file_name = artifact.file_name(crate_type, triple); + self.build_dir(target).join(artifact).join(file_name) } }