diff --git a/Cargo.toml b/Cargo.toml index d1143aa15..ca8f3646d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ glob = "0.3.0" hex = "0.4" home = "0.5" humantime = "2.0.0" +indexmap = "1" ignore = "0.4.7" lazy_static = "1.2.0" jobserver = "0.1.24" @@ -45,7 +46,7 @@ libgit2-sys = "0.13.2" memchr = "2.1.3" opener = "0.5" os_info = "3.0.7" -pathdiff = "0.2.1" +pathdiff = "0.2" percent-encoding = "2.0" rustfix = "0.6.0" semver = { version = "1.0.3", features = ["serde"] } diff --git a/src/bin/cargo/commands/add.rs b/src/bin/cargo/commands/add.rs new file mode 100644 index 000000000..8ed6ea1fa --- /dev/null +++ b/src/bin/cargo/commands/add.rs @@ -0,0 +1,360 @@ +use indexmap::IndexMap; +use indexmap::IndexSet; + +use cargo::core::dependency::DepKind; +use cargo::core::FeatureValue; +use cargo::ops::cargo_add::add; +use cargo::ops::cargo_add::AddOptions; +use cargo::ops::cargo_add::DepOp; +use cargo::ops::cargo_add::DepTable; +use cargo::util::command_prelude::*; +use cargo::util::interning::InternedString; +use cargo::CargoResult; + +pub fn cli() -> clap::Command<'static> { + clap::Command::new("add") + .setting(clap::AppSettings::DeriveDisplayOrder) + .about("Add dependencies to a Cargo.toml manifest file") + .override_usage( + "\ + cargo add [OPTIONS] [@] ... + cargo add [OPTIONS] --path ... + cargo add [OPTIONS] --git ..." + ) + .after_help("Run `cargo help add` for more detailed information.\n") + .group(clap::ArgGroup::new("selected").multiple(true).required(true)) + .args([ + clap::Arg::new("crates") + .takes_value(true) + .value_name("DEP_ID") + .multiple_occurrences(true) + .help("Reference to a package to add as a dependency") + .long_help( + "Reference to a package to add as a dependency + +You can reference a package by: +- ``, like `cargo add serde` (latest version will be used) +- `@`, like `cargo add serde@1` or `cargo add serde@=1.0.38`" + ) + .group("selected"), + clap::Arg::new("no-default-features") + .long("no-default-features") + .help("Disable the default features"), + clap::Arg::new("default-features") + .long("default-features") + .help("Re-enable the default features") + .overrides_with("no-default-features"), + clap::Arg::new("features") + .short('F') + .long("features") + .takes_value(true) + .value_name("FEATURES") + .multiple_occurrences(true) + .help("Space or comma separated list of features to activate"), + clap::Arg::new("optional") + .long("optional") + .help("Mark the dependency as optional") + .long_help("Mark the dependency as optional + +The package name will be exposed as feature of your crate.") + .conflicts_with("dev"), + clap::Arg::new("no-optional") + .long("no-optional") + .help("Mark the dependency as required") + .long_help("Mark the dependency as required + +The package will be removed from your features.") + .conflicts_with("dev") + .overrides_with("optional"), + clap::Arg::new("rename") + .long("rename") + .takes_value(true) + .value_name("NAME") + .help("Rename the dependency") + .long_help("Rename the dependency + +Example uses: +- Depending on multiple versions of a crate +- Depend on crates with the same name from different registries"), + ]) + .arg_manifest_path() + .args([ + clap::Arg::new("package") + .short('p') + .long("package") + .takes_value(true) + .value_name("SPEC") + .help("Package to modify"), + clap::Arg::new("offline") + .long("offline") + .help("Run without accessing the network") + ]) + .arg_quiet() + .arg_dry_run("Don't actually write the manifest") + .next_help_heading("SOURCE") + .args([ + clap::Arg::new("path") + .long("path") + .takes_value(true) + .value_name("PATH") + .help("Filesystem path to local crate to add") + .group("selected") + .conflicts_with("git"), + clap::Arg::new("git") + .long("git") + .takes_value(true) + .value_name("URI") + .help("Git repository location") + .long_help("Git repository location + +Without any other information, cargo will use latest commit on the main branch.") + .group("selected"), + clap::Arg::new("branch") + .long("branch") + .takes_value(true) + .value_name("BRANCH") + .help("Git branch to download the crate from") + .requires("git") + .group("git-ref"), + clap::Arg::new("tag") + .long("tag") + .takes_value(true) + .value_name("TAG") + .help("Git tag to download the crate from") + .requires("git") + .group("git-ref"), + clap::Arg::new("rev") + .long("rev") + .takes_value(true) + .value_name("REV") + .help("Git reference to download the crate from") + .long_help("Git reference to download the crate from + +This is the catch all, handling hashes to named references in remote repositories.") + .requires("git") + .group("git-ref"), + clap::Arg::new("registry") + .long("registry") + .takes_value(true) + .value_name("NAME") + .help("Package registry for this dependency"), + ]) + .next_help_heading("SECTION") + .args([ + clap::Arg::new("dev") + .long("dev") + .help("Add as development dependency") + .long_help("Add as development dependency + +Dev-dependencies are not used when compiling a package for building, but are used for compiling tests, examples, and benchmarks. + +These dependencies are not propagated to other packages which depend on this package.") + .group("section"), + clap::Arg::new("build") + .long("build") + .help("Add as build dependency") + .long_help("Add as build dependency + +Build-dependencies are the only dependencies available for use by build scripts (`build.rs` files).") + .group("section"), + clap::Arg::new("target") + .long("target") + .takes_value(true) + .value_name("TARGET") + .forbid_empty_values(true) + .help("Add as dependency to the given target platform") + ]) +} + +pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + let dry_run = args.is_present("dry-run"); + let section = parse_section(args); + + let ws = args.workspace(config)?; + let packages = args.packages_from_flags()?; + let packages = packages.get_packages(&ws)?; + let spec = match packages.len() { + 0 => { + return Err(CliError::new( + anyhow::format_err!("no packages selected. Please specify one with `-p `"), + 101, + )); + } + 1 => packages[0], + len => { + return Err(CliError::new( + anyhow::format_err!( + "{len} packages selected. Please specify one with `-p `", + ), + 101, + )); + } + }; + + let dependencies = parse_dependencies(config, args)?; + + let options = AddOptions { + config, + spec, + dependencies, + section, + dry_run, + }; + add(&ws, &options)?; + + Ok(()) +} + +fn parse_dependencies(config: &Config, matches: &ArgMatches) -> CargoResult> { + let path = matches.value_of("path"); + let git = matches.value_of("git"); + let branch = matches.value_of("branch"); + let rev = matches.value_of("rev"); + let tag = matches.value_of("tag"); + let rename = matches.value_of("rename"); + let registry = matches.registry(config)?; + let default_features = default_features(matches); + let optional = optional(matches); + + let mut crates = matches + .values_of("crates") + .into_iter() + .flatten() + .map(|c| (Some(String::from(c)), None)) + .collect::>(); + let mut infer_crate_name = false; + if crates.is_empty() { + if path.is_some() || git.is_some() { + crates.insert(None, None); + infer_crate_name = true; + } else { + unreachable!("clap should ensure we have some source selected"); + } + } + for feature in matches + .values_of("features") + .into_iter() + .flatten() + .flat_map(parse_feature) + { + let parsed_value = FeatureValue::new(InternedString::new(feature)); + match parsed_value { + FeatureValue::Feature(_) => { + if 1 < crates.len() { + let candidates = crates + .keys() + .map(|c| { + format!( + "`{}/{}`", + c.as_deref().expect("only none when there is 1"), + feature + ) + }) + .collect::>(); + anyhow::bail!("feature `{feature}` must be qualified by the dependency its being activated for, like {}", candidates.join(", ")); + } + crates + .first_mut() + .expect("always at least one crate") + .1 + .get_or_insert_with(IndexSet::new) + .insert(feature.to_owned()); + } + FeatureValue::Dep { .. } => { + anyhow::bail!("feature `{feature}` is not allowed to use explicit `dep:` syntax",) + } + FeatureValue::DepFeature { + dep_name, + dep_feature, + .. + } => { + if infer_crate_name { + anyhow::bail!("`{feature}` is unsupported when inferring the crate name, use `{dep_feature}`"); + } + if dep_feature.contains('/') { + anyhow::bail!("multiple slashes in feature `{feature}` is not allowed"); + } + crates.get_mut(&Some(dep_name.as_str().to_owned())).ok_or_else(|| { + anyhow::format_err!("feature `{dep_feature}` activated for crate `{dep_name}` but the crate wasn't specified") + })? + .get_or_insert_with(IndexSet::new) + .insert(dep_feature.as_str().to_owned()); + } + } + } + + let mut deps: Vec = Vec::new(); + for (crate_spec, features) in crates { + let dep = DepOp { + crate_spec, + rename: rename.map(String::from), + features, + default_features, + optional, + registry: registry.clone(), + path: path.map(String::from), + git: git.map(String::from), + branch: branch.map(String::from), + rev: rev.map(String::from), + tag: tag.map(String::from), + }; + deps.push(dep); + } + + if deps.len() > 1 && rename.is_some() { + anyhow::bail!("cannot specify multiple crates with `--rename`"); + } + + Ok(deps) +} + +fn default_features(matches: &ArgMatches) -> Option { + resolve_bool_arg( + matches.is_present("default-features"), + matches.is_present("no-default-features"), + ) +} + +fn optional(matches: &ArgMatches) -> Option { + resolve_bool_arg( + matches.is_present("optional"), + matches.is_present("no-optional"), + ) +} + +fn resolve_bool_arg(yes: bool, no: bool) -> Option { + match (yes, no) { + (true, false) => Some(true), + (false, true) => Some(false), + (false, false) => None, + (_, _) => unreachable!("clap should make this impossible"), + } +} + +fn parse_section(matches: &ArgMatches) -> DepTable { + let kind = if matches.is_present("dev") { + DepKind::Development + } else if matches.is_present("build") { + DepKind::Build + } else { + DepKind::Normal + }; + + let mut table = DepTable::new().set_kind(kind); + + if let Some(target) = matches.value_of("target") { + assert!(!target.is_empty(), "Target specification may not be empty"); + table = table.set_target(target); + } + + table +} + +/// Split feature flag list +fn parse_feature(feature: &str) -> impl Iterator { + // Not re-using `CliFeatures` because it uses a BTreeSet and loses user's ordering + feature + .split_whitespace() + .flat_map(|s| s.split(',')) + .filter(|s| !s.is_empty()) +} diff --git a/src/bin/cargo/commands/mod.rs b/src/bin/cargo/commands/mod.rs index 7d77388f5..92ee7d75e 100644 --- a/src/bin/cargo/commands/mod.rs +++ b/src/bin/cargo/commands/mod.rs @@ -2,6 +2,7 @@ use crate::command_prelude::*; pub fn builtin() -> Vec { vec![ + add::cli(), bench::cli(), build::cli(), check::cli(), @@ -42,6 +43,7 @@ pub fn builtin() -> Vec { pub fn builtin_exec(cmd: &str) -> Option CliResult> { let f = match cmd { + "add" => add::exec, "bench" => bench::exec, "build" => build::exec, "check" => check::exec, @@ -82,6 +84,7 @@ pub fn builtin_exec(cmd: &str) -> Option CliResu Some(f) } +pub mod add; pub mod bench; pub mod build; pub mod check; diff --git a/src/cargo/ops/cargo_add/crate_spec.rs b/src/cargo/ops/cargo_add/crate_spec.rs new file mode 100644 index 000000000..33b4957f7 --- /dev/null +++ b/src/cargo/ops/cargo_add/crate_spec.rs @@ -0,0 +1,63 @@ +//! Crate name parsing. + +use anyhow::Context as _; + +use super::Dependency; +use super::RegistrySource; +use crate::util::validate_package_name; +use crate::CargoResult; + +/// User-specified crate +/// +/// This can be a +/// - Name (e.g. `docopt`) +/// - Name and a version req (e.g. `docopt@^0.8`) +/// - Path +#[derive(Debug)] +pub struct CrateSpec { + /// Crate name + name: String, + /// Optional version requirement + version_req: Option, +} + +impl CrateSpec { + /// Convert a string to a `Crate` + pub fn resolve(pkg_id: &str) -> CargoResult { + let (name, version) = pkg_id + .split_once('@') + .map(|(n, v)| (n, Some(v))) + .unwrap_or((pkg_id, None)); + + validate_package_name(name, "dependency name", "")?; + + if let Some(version) = version { + semver::VersionReq::parse(version) + .with_context(|| format!("invalid version requirement `{version}`"))?; + } + + let id = Self { + name: name.to_owned(), + version_req: version.map(|s| s.to_owned()), + }; + + Ok(id) + } + + /// Generate a dependency entry for this crate specifier + pub fn to_dependency(&self) -> CargoResult { + let mut dep = Dependency::new(self.name()); + if let Some(version_req) = self.version_req() { + dep = dep.set_source(RegistrySource::new(version_req)); + } + Ok(dep) + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn version_req(&self) -> Option<&str> { + self.version_req.as_deref() + } +} diff --git a/src/cargo/ops/cargo_add/dependency.rs b/src/cargo/ops/cargo_add/dependency.rs new file mode 100644 index 000000000..23b3373fc --- /dev/null +++ b/src/cargo/ops/cargo_add/dependency.rs @@ -0,0 +1,1038 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use indexmap::IndexSet; + +use super::manifest::str_or_1_len_table; +use crate::core::FeatureMap; +use crate::core::FeatureValue; +use crate::core::GitReference; +use crate::core::SourceId; +use crate::core::Summary; +use crate::CargoResult; +use crate::Config; + +/// A dependency handled by Cargo +/// +/// `None` means the field will be blank in TOML +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct Dependency { + /// The name of the dependency (as it is set in its `Cargo.toml` and known to crates.io) + pub name: String, + /// Whether the dependency is opted-in with a feature flag + pub optional: Option, + + /// List of features to add (or None to keep features unchanged). + pub features: Option>, + /// Whether default features are enabled + pub default_features: Option, + + /// Where the dependency comes from + pub source: Option, + /// Non-default registry + pub registry: Option, + + /// If the dependency is renamed, this is the new name for the dependency + /// as a string. None if it is not renamed. + pub rename: Option, + + /// Features that are exposed by the dependency + pub available_features: BTreeMap>, +} + +impl Dependency { + /// Create a new dependency with a name + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + optional: None, + features: None, + default_features: None, + source: None, + registry: None, + rename: None, + available_features: Default::default(), + } + } + + /// Set dependency to a given version + pub fn set_source(mut self, source: impl Into) -> Self { + self.source = Some(source.into()); + self + } + + /// Remove the existing version requirement + pub fn clear_version(mut self) -> Self { + match &mut self.source { + Some(Source::Registry(_)) => { + self.source = None; + } + Some(Source::Path(path)) => { + path.version = None; + } + Some(Source::Git(git)) => { + git.version = None; + } + None => {} + } + self + } + + /// Set the available features of the dependency to a given vec + pub fn set_available_features( + mut self, + available_features: BTreeMap>, + ) -> Self { + self.available_features = available_features; + self + } + + /// Populate from cargo + pub fn set_available_features_from_cargo( + mut self, + available_features: &FeatureMap, + ) -> Dependency { + self.available_features = available_features + .iter() + .map(|(k, v)| { + ( + k.as_str().to_owned(), + v.iter() + .filter_map(|v| match v { + FeatureValue::Feature(f) => Some(f.as_str().to_owned()), + FeatureValue::Dep { .. } | FeatureValue::DepFeature { .. } => None, + }) + .collect::>(), + ) + }) + .collect(); + self + } + + /// Set whether the dependency is optional + #[allow(dead_code)] + pub fn set_optional(mut self, opt: bool) -> Self { + self.optional = Some(opt); + self + } + + /// Set features as an array of string (does some basic parsing) + #[allow(dead_code)] + pub fn set_features(mut self, features: IndexSet) -> Self { + self.features = Some(features); + self + } + /// Set features as an array of string (does some basic parsing) + pub fn extend_features(mut self, features: impl IntoIterator) -> Self { + self.features + .get_or_insert_with(Default::default) + .extend(features); + self + } + + /// Set the value of default-features for the dependency + #[allow(dead_code)] + pub fn set_default_features(mut self, default_features: bool) -> Self { + self.default_features = Some(default_features); + self + } + + /// Set the alias for the dependency + pub fn set_rename(mut self, rename: &str) -> Self { + self.rename = Some(rename.into()); + self + } + + /// Set the value of registry for the dependency + pub fn set_registry(mut self, registry: impl Into) -> Self { + self.registry = Some(registry.into()); + self + } + + /// Get the dependency source + pub fn source(&self) -> Option<&Source> { + self.source.as_ref() + } + + /// Get version of dependency + pub fn version(&self) -> Option<&str> { + match self.source()? { + Source::Registry(src) => Some(src.version.as_str()), + Source::Path(src) => src.version.as_deref(), + Source::Git(src) => src.version.as_deref(), + } + } + + /// Get registry of the dependency + pub fn registry(&self) -> Option<&str> { + self.registry.as_deref() + } + + /// Get the alias for the dependency (if any) + pub fn rename(&self) -> Option<&str> { + self.rename.as_deref() + } + + /// Whether default features are activated + pub fn default_features(&self) -> Option { + self.default_features + } + + /// Get whether the dep is optional + pub fn optional(&self) -> Option { + self.optional + } + + /// Get the SourceID for this dependency + pub fn source_id(&self, config: &Config) -> CargoResult { + match &self.source.as_ref() { + Some(Source::Registry(_)) | None => { + if let Some(r) = self.registry() { + let source_id = SourceId::alt_registry(config, r)?; + Ok(source_id) + } else { + let source_id = SourceId::crates_io(config)?; + Ok(source_id) + } + } + Some(Source::Path(source)) => source.source_id(), + Some(Source::Git(source)) => source.source_id(), + } + } + + /// Query to find this dependency + pub fn query(&self, config: &Config) -> CargoResult { + let source_id = self.source_id(config)?; + crate::core::dependency::Dependency::parse(self.name.as_str(), self.version(), source_id) + } +} + +impl Dependency { + /// Create a dependency from a TOML table entry + pub fn from_toml(crate_root: &Path, key: &str, item: &toml_edit::Item) -> CargoResult { + if let Some(version) = item.as_str() { + let dep = Self::new(key).set_source(RegistrySource::new(version)); + Ok(dep) + } else if let Some(table) = item.as_table_like() { + let (name, rename) = if let Some(value) = table.get("package") { + ( + value + .as_str() + .ok_or_else(|| invalid_type(key, "package", value.type_name(), "string"))? + .to_owned(), + Some(key.to_owned()), + ) + } else { + (key.to_owned(), None) + }; + + let source: Source = + if let Some(git) = table.get("git") { + let mut src = GitSource::new( + git.as_str() + .ok_or_else(|| invalid_type(key, "git", git.type_name(), "string"))?, + ); + if let Some(value) = table.get("branch") { + src = src.set_branch(value.as_str().ok_or_else(|| { + invalid_type(key, "branch", value.type_name(), "string") + })?); + } + if let Some(value) = table.get("tag") { + src = src.set_tag(value.as_str().ok_or_else(|| { + invalid_type(key, "tag", value.type_name(), "string") + })?); + } + if let Some(value) = table.get("rev") { + src = src.set_rev(value.as_str().ok_or_else(|| { + invalid_type(key, "rev", value.type_name(), "string") + })?); + } + if let Some(value) = table.get("version") { + src = src.set_version(value.as_str().ok_or_else(|| { + invalid_type(key, "version", value.type_name(), "string") + })?); + } + src.into() + } else if let Some(path) = table.get("path") { + let path = crate_root + .join(path.as_str().ok_or_else(|| { + invalid_type(key, "path", path.type_name(), "string") + })?); + let mut src = PathSource::new(path); + if let Some(value) = table.get("version") { + src = src.set_version(value.as_str().ok_or_else(|| { + invalid_type(key, "version", value.type_name(), "string") + })?); + } + src.into() + } else if let Some(version) = table.get("version") { + let src = RegistrySource::new(version.as_str().ok_or_else(|| { + invalid_type(key, "version", version.type_name(), "string") + })?); + src.into() + } else { + anyhow::bail!("Unrecognized dependency source for `{key}`"); + }; + let registry = if let Some(value) = table.get("registry") { + Some( + value + .as_str() + .ok_or_else(|| invalid_type(key, "registry", value.type_name(), "string"))? + .to_owned(), + ) + } else { + None + }; + + let default_features = table.get("default-features").and_then(|v| v.as_bool()); + if table.contains_key("default_features") { + anyhow::bail!("Use of `default_features` in `{key}` is unsupported, please switch to `default-features`"); + } + + let features = if let Some(value) = table.get("features") { + Some( + value + .as_array() + .ok_or_else(|| invalid_type(key, "features", value.type_name(), "array"))? + .iter() + .map(|v| { + v.as_str().map(|s| s.to_owned()).ok_or_else(|| { + invalid_type(key, "features", v.type_name(), "string") + }) + }) + .collect::>>()?, + ) + } else { + None + }; + + let available_features = BTreeMap::default(); + + let optional = table.get("optional").and_then(|v| v.as_bool()); + + let dep = Self { + name, + rename, + source: Some(source), + registry, + default_features, + features, + available_features, + optional, + }; + Ok(dep) + } else { + anyhow::bail!("Unrecognized` dependency entry format for `{key}"); + } + } + + /// Get the dependency name as defined in the manifest, + /// that is, either the alias (rename field if Some), + /// or the official package name (name field). + pub fn toml_key(&self) -> &str { + self.rename().unwrap_or(&self.name) + } + + /// Convert dependency to TOML + /// + /// Returns a tuple with the dependency's name and either the version as a `String` + /// or the path/git repository as an `InlineTable`. + /// (If the dependency is set as `optional` or `default-features` is set to `false`, + /// an `InlineTable` is returned in any case.) + /// + /// # Panic + /// + /// Panics if the path is relative + pub fn to_toml(&self, crate_root: &Path) -> toml_edit::Item { + assert!( + crate_root.is_absolute(), + "Absolute path needed, got: {}", + crate_root.display() + ); + let table: toml_edit::Item = match ( + self.optional.unwrap_or(false), + self.features.as_ref(), + self.default_features.unwrap_or(true), + self.source.as_ref(), + self.registry.as_ref(), + self.rename.as_ref(), + ) { + // Extra short when version flag only + ( + false, + None, + true, + Some(Source::Registry(RegistrySource { version: v })), + None, + None, + ) => toml_edit::value(v), + // Other cases are represented as an inline table + (_, _, _, _, _, _) => { + let mut table = toml_edit::InlineTable::default(); + + match &self.source { + Some(Source::Registry(src)) => { + table.insert("version", src.version.as_str().into()); + } + Some(Source::Path(src)) => { + let relpath = path_field(crate_root, &src.path); + if let Some(r) = src.version.as_deref() { + table.insert("version", r.into()); + } + table.insert("path", relpath.into()); + } + Some(Source::Git(src)) => { + table.insert("git", src.git.as_str().into()); + if let Some(branch) = src.branch.as_deref() { + table.insert("branch", branch.into()); + } + if let Some(tag) = src.tag.as_deref() { + table.insert("tag", tag.into()); + } + if let Some(rev) = src.rev.as_deref() { + table.insert("rev", rev.into()); + } + if let Some(r) = src.version.as_deref() { + table.insert("version", r.into()); + } + } + None => {} + } + if table.contains_key("version") { + if let Some(r) = self.registry.as_deref() { + table.insert("registry", r.into()); + } + } + + if self.rename.is_some() { + table.insert("package", self.name.as_str().into()); + } + if let Some(v) = self.default_features { + table.insert("default-features", v.into()); + } + if let Some(features) = self.features.as_ref() { + let features: toml_edit::Value = features.iter().cloned().collect(); + table.insert("features", features); + } + if let Some(v) = self.optional { + table.insert("optional", v.into()); + } + + toml_edit::value(toml_edit::Value::InlineTable(table)) + } + }; + + table + } + + /// Modify existing entry to match this dependency + pub fn update_toml(&self, crate_root: &Path, item: &mut toml_edit::Item) { + if str_or_1_len_table(item) { + // Nothing to preserve + *item = self.to_toml(crate_root); + } else if let Some(table) = item.as_table_like_mut() { + match &self.source { + Some(Source::Registry(src)) => { + table.insert("version", toml_edit::value(src.version.as_str())); + + for key in ["path", "git", "branch", "tag", "rev"] { + table.remove(key); + } + } + Some(Source::Path(src)) => { + let relpath = path_field(crate_root, &src.path); + table.insert("path", toml_edit::value(relpath)); + if let Some(r) = src.version.as_deref() { + table.insert("version", toml_edit::value(r)); + } else { + table.remove("version"); + } + + for key in ["git", "branch", "tag", "rev"] { + table.remove(key); + } + } + Some(Source::Git(src)) => { + table.insert("git", toml_edit::value(src.git.as_str())); + if let Some(branch) = src.branch.as_deref() { + table.insert("branch", toml_edit::value(branch)); + } else { + table.remove("branch"); + } + if let Some(tag) = src.tag.as_deref() { + table.insert("tag", toml_edit::value(tag)); + } else { + table.remove("tag"); + } + if let Some(rev) = src.rev.as_deref() { + table.insert("rev", toml_edit::value(rev)); + } else { + table.remove("rev"); + } + if let Some(r) = src.version.as_deref() { + table.insert("version", toml_edit::value(r)); + } else { + table.remove("version"); + } + + for key in ["path"] { + table.remove(key); + } + } + None => {} + } + if table.contains_key("version") { + if let Some(r) = self.registry.as_deref() { + table.insert("registry", toml_edit::value(r)); + } else { + table.remove("registry"); + } + } else { + table.remove("registry"); + } + + if self.rename.is_some() { + table.insert("package", toml_edit::value(self.name.as_str())); + } + match self.default_features { + Some(v) => { + table.insert("default-features", toml_edit::value(v)); + } + None => { + table.remove("default-features"); + } + } + if let Some(new_features) = self.features.as_ref() { + let mut features = table + .get("features") + .and_then(|i| i.as_value()) + .and_then(|v| v.as_array()) + .and_then(|a| { + a.iter() + .map(|v| v.as_str()) + .collect::>>() + }) + .unwrap_or_default(); + features.extend(new_features.iter().map(|s| s.as_str())); + let features = toml_edit::value(features.into_iter().collect::()); + table.insert("features", features); + } else { + table.remove("features"); + } + match self.optional { + Some(v) => { + table.insert("optional", toml_edit::value(v)); + } + None => { + table.remove("optional"); + } + } + + table.fmt(); + } else { + unreachable!("Invalid dependency type: {}", item.type_name()); + } + } +} + +fn invalid_type(dep: &str, key: &str, actual: &str, expected: &str) -> anyhow::Error { + anyhow::format_err!("Found {actual} for {key} when {expected} was expected for {dep}") +} + +impl std::fmt::Display for Dependency { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(source) = self.source() { + write!(f, "{}@{}", self.name, source) + } else { + self.toml_key().fmt(f) + } + } +} + +impl<'s> From<&'s Summary> for Dependency { + fn from(other: &'s Summary) -> Self { + let source: Source = if let Some(path) = other.source_id().local_path() { + PathSource::new(path) + .set_version(other.version().to_string()) + .into() + } else if let Some(git_ref) = other.source_id().git_reference() { + let mut src = GitSource::new(other.source_id().url().to_string()) + .set_version(other.version().to_string()); + match git_ref { + GitReference::Branch(branch) => src = src.set_branch(branch), + GitReference::Tag(tag) => src = src.set_tag(tag), + GitReference::Rev(rev) => src = src.set_rev(rev), + GitReference::DefaultBranch => {} + } + src.into() + } else { + RegistrySource::new(other.version().to_string()).into() + }; + Dependency::new(other.name().as_str()) + .set_source(source) + .set_available_features_from_cargo(other.features()) + } +} + +impl From for Dependency { + fn from(other: Summary) -> Self { + (&other).into() + } +} + +fn path_field(crate_root: &Path, abs_path: &Path) -> String { + let relpath = pathdiff::diff_paths(abs_path, crate_root).expect("both paths are absolute"); + let relpath = relpath.to_str().unwrap().replace('\\', "/"); + relpath +} + +/// Primary location of a dependency +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +pub enum Source { + /// Dependency from a registry + Registry(RegistrySource), + /// Dependency from a local path + Path(PathSource), + /// Dependency from a git repo + Git(GitSource), +} + +impl Source { + /// Access the registry source, if present + pub fn as_registry(&self) -> Option<&RegistrySource> { + match self { + Self::Registry(src) => Some(src), + _ => None, + } + } + + /// Access the path source, if present + #[allow(dead_code)] + pub fn as_path(&self) -> Option<&PathSource> { + match self { + Self::Path(src) => Some(src), + _ => None, + } + } + + /// Access the git source, if present + #[allow(dead_code)] + pub fn as_git(&self) -> Option<&GitSource> { + match self { + Self::Git(src) => Some(src), + _ => None, + } + } +} + +impl std::fmt::Display for Source { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Registry(src) => src.fmt(f), + Self::Path(src) => src.fmt(f), + Self::Git(src) => src.fmt(f), + } + } +} + +impl<'s> From<&'s Source> for Source { + fn from(inner: &'s Source) -> Self { + inner.clone() + } +} + +impl From for Source { + fn from(inner: RegistrySource) -> Self { + Self::Registry(inner) + } +} + +impl From for Source { + fn from(inner: PathSource) -> Self { + Self::Path(inner) + } +} + +impl From for Source { + fn from(inner: GitSource) -> Self { + Self::Git(inner) + } +} + +/// Dependency from a registry +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct RegistrySource { + /// Version requirement + pub version: String, +} + +impl RegistrySource { + /// Specify dependency by version requirement + pub fn new(version: impl AsRef) -> Self { + // versions might have semver metadata appended which we do not want to + // store in the cargo toml files. This would cause a warning upon compilation + // ("version requirement […] includes semver metadata which will be ignored") + let version = version.as_ref().split('+').next().unwrap(); + Self { + version: version.to_owned(), + } + } +} + +impl std::fmt::Display for RegistrySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.version.fmt(f) + } +} + +/// Dependency from a local path +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct PathSource { + /// Local, absolute path + pub path: PathBuf, + /// Version requirement for when published + pub version: Option, +} + +impl PathSource { + /// Specify dependency from a path + pub fn new(path: impl Into) -> Self { + Self { + path: path.into(), + version: None, + } + } + + /// Set an optional version requirement + pub fn set_version(mut self, version: impl AsRef) -> Self { + // versions might have semver metadata appended which we do not want to + // store in the cargo toml files. This would cause a warning upon compilation + // ("version requirement […] includes semver metadata which will be ignored") + let version = version.as_ref().split('+').next().unwrap(); + self.version = Some(version.to_owned()); + self + } + + /// Get the SourceID for this dependency + pub fn source_id(&self) -> CargoResult { + SourceId::for_path(&self.path) + } +} + +impl std::fmt::Display for PathSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.path.display().fmt(f) + } +} + +/// Dependency from a git repo +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct GitSource { + /// Repo URL + pub git: String, + /// Select specific branch + pub branch: Option, + /// Select specific tag + pub tag: Option, + /// Select specific rev + pub rev: Option, + /// Version requirement for when published + pub version: Option, +} + +impl GitSource { + /// Specify dependency from a git repo + pub fn new(git: impl Into) -> Self { + Self { + git: git.into(), + branch: None, + tag: None, + rev: None, + version: None, + } + } + + /// Specify an optional branch + pub fn set_branch(mut self, branch: impl Into) -> Self { + self.branch = Some(branch.into()); + self.tag = None; + self.rev = None; + self + } + + /// Specify an optional tag + pub fn set_tag(mut self, tag: impl Into) -> Self { + self.branch = None; + self.tag = Some(tag.into()); + self.rev = None; + self + } + + /// Specify an optional rev + pub fn set_rev(mut self, rev: impl Into) -> Self { + self.branch = None; + self.tag = None; + self.rev = Some(rev.into()); + self + } + + /// Get the SourceID for this dependency + pub fn source_id(&self) -> CargoResult { + let git_url = self.git.parse::()?; + let git_ref = self.git_ref(); + SourceId::for_git(&git_url, git_ref) + } + + fn git_ref(&self) -> GitReference { + match ( + self.branch.as_deref(), + self.tag.as_deref(), + self.rev.as_deref(), + ) { + (Some(branch), _, _) => GitReference::Branch(branch.to_owned()), + (_, Some(tag), _) => GitReference::Tag(tag.to_owned()), + (_, _, Some(rev)) => GitReference::Rev(rev.to_owned()), + _ => GitReference::DefaultBranch, + } + } + + /// Set an optional version requirement + pub fn set_version(mut self, version: impl AsRef) -> Self { + // versions might have semver metadata appended which we do not want to + // store in the cargo toml files. This would cause a warning upon compilation + // ("version requirement […] includes semver metadata which will be ignored") + let version = version.as_ref().split('+').next().unwrap(); + self.version = Some(version.to_owned()); + self + } +} + +impl std::fmt::Display for GitSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let git_ref = self.git_ref(); + if let Some(pretty_ref) = git_ref.pretty_ref() { + write!(f, "{}?{}", self.git, pretty_ref) + } else { + write!(f, "{}", self.git) + } + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use cargo_util::paths; + + use super::*; + + #[test] + fn to_toml_simple_dep() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0")); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_simple_dep_with_version() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep").set_source(RegistrySource::new("1.0")); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert_eq!(item.as_str(), Some("1.0")); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_optional_dep() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep") + .set_source(RegistrySource::new("1.0")) + .set_optional(true); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("optional").unwrap().as_bool(), Some(true)); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_dep_without_default_features() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep") + .set_source(RegistrySource::new("1.0")) + .set_default_features(false); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false)); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_dep_with_path_source() { + let root = paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let crate_root = root.join("foo"); + let dep = Dependency::new("dep").set_source(PathSource::new(root.join("bar"))); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("path").unwrap().as_str(), Some("../bar")); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_dep_with_git_source() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep").set_source(GitSource::new("https://foor/bar.git")); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!( + dep.get("git").unwrap().as_str(), + Some("https://foor/bar.git") + ); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_renamed_dep() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep") + .set_source(RegistrySource::new("1.0")) + .set_rename("d"); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "d".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("package").unwrap().as_str(), Some("dep")); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_dep_from_alt_registry() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep") + .set_source(RegistrySource::new("1.0")) + .set_registry("alternative"); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "dep".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("registry").unwrap().as_str(), Some("alternative")); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn to_toml_complex_dep() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let dep = Dependency::new("dep") + .set_source(RegistrySource::new("1.0")) + .set_default_features(false) + .set_rename("d"); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + assert_eq!(key, "d".to_owned()); + assert!(item.is_inline_table()); + + let dep = item.as_inline_table().unwrap(); + assert_eq!(dep.get("package").unwrap().as_str(), Some("dep")); + assert_eq!(dep.get("version").unwrap().as_str(), Some("1.0")); + assert_eq!(dep.get("default-features").unwrap().as_bool(), Some(false)); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + fn paths_with_forward_slashes_are_left_as_is() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let path = crate_root.join("sibling/crate"); + let relpath = "sibling/crate"; + let dep = Dependency::new("dep").set_source(PathSource::new(path)); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + let table = item.as_inline_table().unwrap(); + let got = table.get("path").unwrap().as_str().unwrap(); + assert_eq!(got, relpath); + + verify_roundtrip(&crate_root, key, &item); + } + + #[test] + #[cfg(windows)] + fn normalise_windows_style_paths() { + let crate_root = + paths::normalize_path(&std::env::current_dir().unwrap().join(Path::new("/"))); + let original = crate_root.join(r"sibling\crate"); + let should_be = "sibling/crate"; + let dep = Dependency::new("dep").set_source(PathSource::new(original)); + let key = dep.toml_key(); + let item = dep.to_toml(&crate_root); + + let table = item.as_inline_table().unwrap(); + let got = table.get("path").unwrap().as_str().unwrap(); + assert_eq!(got, should_be); + + verify_roundtrip(&crate_root, key, &item); + } + + #[track_caller] + fn verify_roundtrip(crate_root: &Path, key: &str, item: &toml_edit::Item) { + let roundtrip = Dependency::from_toml(crate_root, key, item).unwrap(); + let round_key = roundtrip.toml_key(); + let round_item = roundtrip.to_toml(crate_root); + assert_eq!(key, round_key); + assert_eq!(item.to_string(), round_item.to_string()); + } +} diff --git a/src/cargo/ops/cargo_add/manifest.rs b/src/cargo/ops/cargo_add/manifest.rs new file mode 100644 index 000000000..63cab6a70 --- /dev/null +++ b/src/cargo/ops/cargo_add/manifest.rs @@ -0,0 +1,507 @@ +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; +use std::str; + +use anyhow::Context as _; + +use super::dependency::Dependency; +use crate::core::dependency::DepKind; +use crate::core::FeatureValue; +use crate::util::interning::InternedString; +use crate::CargoResult; + +/// Dependency table to add dep to +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DepTable { + kind: DepKind, + target: Option, +} + +impl DepTable { + const KINDS: &'static [Self] = &[ + Self::new().set_kind(DepKind::Normal), + Self::new().set_kind(DepKind::Development), + Self::new().set_kind(DepKind::Build), + ]; + + /// Reference to a Dependency Table + pub const fn new() -> Self { + Self { + kind: DepKind::Normal, + target: None, + } + } + + /// Choose the type of dependency + pub const fn set_kind(mut self, kind: DepKind) -> Self { + self.kind = kind; + self + } + + /// Choose the platform for the dependency + pub fn set_target(mut self, target: impl Into) -> Self { + self.target = Some(target.into()); + self + } + + /// Type of dependency + pub fn kind(&self) -> DepKind { + self.kind + } + + /// Platform for the dependency + pub fn target(&self) -> Option<&str> { + self.target.as_deref() + } + + /// Keys to the table + pub fn to_table(&self) -> Vec<&str> { + if let Some(target) = &self.target { + vec!["target", target, self.kind_table()] + } else { + vec![self.kind_table()] + } + } + + fn kind_table(&self) -> &str { + match self.kind { + DepKind::Normal => "dependencies", + DepKind::Development => "dev-dependencies", + DepKind::Build => "build-dependencies", + } + } +} + +impl Default for DepTable { + fn default() -> Self { + Self::new() + } +} + +impl From for DepTable { + fn from(other: DepKind) -> Self { + Self::new().set_kind(other) + } +} + +/// A Cargo manifest +#[derive(Debug, Clone)] +pub struct Manifest { + /// Manifest contents as TOML data + pub data: toml_edit::Document, +} + +impl Manifest { + /// Get the manifest's package name + pub fn package_name(&self) -> CargoResult<&str> { + self.data + .as_table() + .get("package") + .and_then(|m| m.get("name")) + .and_then(|m| m.as_str()) + .ok_or_else(parse_manifest_err) + } + + /// Get the specified table from the manifest. + pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> { + /// Descend into a manifest until the required table is found. + fn descend<'a>( + input: &'a toml_edit::Item, + path: &[String], + ) -> CargoResult<&'a toml_edit::Item> { + if let Some(segment) = path.get(0) { + let value = input + .get(&segment) + .ok_or_else(|| non_existent_table_err(segment))?; + + if value.is_table_like() { + descend(value, &path[1..]) + } else { + Err(non_existent_table_err(segment)) + } + } else { + Ok(input) + } + } + + descend(self.data.as_item(), table_path) + } + + /// Get the specified table from the manifest. + pub fn get_table_mut<'a>( + &'a mut self, + table_path: &[String], + ) -> CargoResult<&'a mut toml_edit::Item> { + /// Descend into a manifest until the required table is found. + fn descend<'a>( + input: &'a mut toml_edit::Item, + path: &[String], + ) -> CargoResult<&'a mut toml_edit::Item> { + if let Some(segment) = path.get(0) { + let mut default_table = toml_edit::Table::new(); + default_table.set_implicit(true); + let value = input[&segment].or_insert(toml_edit::Item::Table(default_table)); + + if value.is_table_like() { + descend(value, &path[1..]) + } else { + Err(non_existent_table_err(segment)) + } + } else { + Ok(input) + } + } + + descend(self.data.as_item_mut(), table_path) + } + + /// Get all sections in the manifest that exist and might contain dependencies. + /// The returned items are always `Table` or `InlineTable`. + pub fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> { + let mut sections = Vec::new(); + + for table in DepTable::KINDS { + let dependency_type = table.kind_table(); + // Dependencies can be in the three standard sections... + if self + .data + .get(dependency_type) + .map(|t| t.is_table_like()) + .unwrap_or(false) + { + sections.push((table.clone(), self.data[dependency_type].clone())) + } + + // ... and in `target..(build-/dev-)dependencies`. + let target_sections = self + .data + .as_table() + .get("target") + .and_then(toml_edit::Item::as_table_like) + .into_iter() + .flat_map(toml_edit::TableLike::iter) + .filter_map(|(target_name, target_table)| { + let dependency_table = target_table.get(dependency_type)?; + dependency_table.as_table_like().map(|_| { + ( + table.clone().set_target(target_name), + dependency_table.clone(), + ) + }) + }); + + sections.extend(target_sections); + } + + sections + } + + pub fn get_legacy_sections(&self) -> Vec { + let mut result = Vec::new(); + + for dependency_type in ["dev_dependencies", "build_dependencies"] { + if self.data.contains_key(dependency_type) { + result.push(dependency_type.to_owned()); + } + + // ... and in `target..(build-/dev-)dependencies`. + result.extend( + self.data + .as_table() + .get("target") + .and_then(toml_edit::Item::as_table_like) + .into_iter() + .flat_map(toml_edit::TableLike::iter) + .filter_map(|(target_name, target_table)| { + if target_table.as_table_like()?.contains_key(dependency_type) { + Some(format!("target.{target_name}.{dependency_type}")) + } else { + None + } + }), + ); + } + result + } +} + +impl str::FromStr for Manifest { + type Err = anyhow::Error; + + /// Read manifest data from string + fn from_str(input: &str) -> ::std::result::Result { + let d: toml_edit::Document = input.parse().context("Manifest not valid TOML")?; + + Ok(Manifest { data: d }) + } +} + +impl std::fmt::Display for Manifest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = self.data.to_string(); + s.fmt(f) + } +} + +/// A Cargo manifest that is available locally. +#[derive(Debug)] +pub struct LocalManifest { + /// Path to the manifest + pub path: PathBuf, + /// Manifest contents + pub manifest: Manifest, +} + +impl Deref for LocalManifest { + type Target = Manifest; + + fn deref(&self) -> &Manifest { + &self.manifest + } +} + +impl DerefMut for LocalManifest { + fn deref_mut(&mut self) -> &mut Manifest { + &mut self.manifest + } +} + +impl LocalManifest { + /// Construct the `LocalManifest` corresponding to the `Path` provided. + pub fn try_new(path: &Path) -> CargoResult { + if !path.is_absolute() { + anyhow::bail!("can only edit absolute paths, got {}", path.display()); + } + let data = cargo_util::paths::read(&path)?; + let manifest = data.parse().context("Unable to parse Cargo.toml")?; + Ok(LocalManifest { + manifest, + path: path.to_owned(), + }) + } + + /// Write changes back to the file + pub fn write(&self) -> CargoResult<()> { + if !self.manifest.data.contains_key("package") + && !self.manifest.data.contains_key("project") + { + if self.manifest.data.contains_key("workspace") { + anyhow::bail!( + "found virtual manifest at {}, but this command requires running against an \ + actual package in this workspace.", + self.path.display() + ); + } else { + anyhow::bail!( + "missing expected `package` or `project` fields in {}", + self.path.display() + ); + } + } + + let s = self.manifest.data.to_string(); + let new_contents_bytes = s.as_bytes(); + + cargo_util::paths::write(&self.path, new_contents_bytes) + } + + /// Lookup a dependency + pub fn get_dependency_versions<'s>( + &'s self, + dep_key: &'s str, + ) -> impl Iterator)> + 's { + let crate_root = self.path.parent().expect("manifest path is absolute"); + self.get_sections() + .into_iter() + .filter_map(move |(table_path, table)| { + let table = table.into_table().ok()?; + Some( + table + .into_iter() + .filter_map(|(key, item)| { + if key.as_str() == dep_key { + Some((table_path.clone(), key, item)) + } else { + None + } + }) + .collect::>(), + ) + }) + .flatten() + .map(move |(table_path, dep_key, dep_item)| { + let dep = Dependency::from_toml(crate_root, &dep_key, &dep_item); + (table_path, dep) + }) + } + + /// Add entry to a Cargo.toml. + pub fn insert_into_table( + &mut self, + table_path: &[String], + dep: &Dependency, + ) -> CargoResult<()> { + let crate_root = self + .path + .parent() + .expect("manifest path is absolute") + .to_owned(); + let dep_key = dep.toml_key(); + + let table = self.get_table_mut(table_path)?; + if let Some(dep_item) = table.as_table_like_mut().unwrap().get_mut(dep_key) { + dep.update_toml(&crate_root, dep_item); + } else { + let new_dependency = dep.to_toml(&crate_root); + table[dep_key] = new_dependency; + } + if let Some(t) = table.as_inline_table_mut() { + t.fmt() + } + + Ok(()) + } + + /// Remove references to `dep_key` if its no longer present + pub fn gc_dep(&mut self, dep_key: &str) { + let explicit_dep_activation = self.is_explicit_dep_activation(dep_key); + let status = self.dep_status(dep_key); + + if let Some(toml_edit::Item::Table(feature_table)) = + self.data.as_table_mut().get_mut("features") + { + for (_feature, mut feature_values) in feature_table.iter_mut() { + if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) = + &mut feature_values + { + fix_feature_activations( + feature_values, + dep_key, + status, + explicit_dep_activation, + ); + } + } + } + } + + fn is_explicit_dep_activation(&self, dep_key: &str) -> bool { + if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") { + for values in feature_table + .iter() + .map(|(_, a)| a) + .filter_map(|i| i.as_value()) + .filter_map(|v| v.as_array()) + { + for value in values.iter().filter_map(|v| v.as_str()) { + let value = FeatureValue::new(InternedString::new(value)); + if let FeatureValue::Dep { dep_name } = &value { + if dep_name.as_str() == dep_key { + return true; + } + } + } + } + } + + false + } + + fn dep_status(&self, dep_key: &str) -> DependencyStatus { + let mut status = DependencyStatus::None; + for (_, tbl) in self.get_sections() { + if let toml_edit::Item::Table(tbl) = tbl { + if let Some(dep_item) = tbl.get(dep_key) { + let optional = dep_item + .get("optional") + .and_then(|i| i.as_value()) + .and_then(|i| i.as_bool()) + .unwrap_or(false); + if optional { + return DependencyStatus::Optional; + } else { + status = DependencyStatus::Required; + } + } + } + } + status + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum DependencyStatus { + None, + Optional, + Required, +} + +fn fix_feature_activations( + feature_values: &mut toml_edit::Array, + dep_key: &str, + status: DependencyStatus, + explicit_dep_activation: bool, +) { + let remove_list: Vec = feature_values + .iter() + .enumerate() + .filter_map(|(idx, value)| value.as_str().map(|s| (idx, s))) + .filter_map(|(idx, value)| { + let parsed_value = FeatureValue::new(InternedString::new(value)); + match status { + DependencyStatus::None => match (parsed_value, explicit_dep_activation) { + (FeatureValue::Feature(dep_name), false) + | (FeatureValue::Dep { dep_name }, _) + | (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key, + _ => false, + }, + DependencyStatus::Optional => false, + DependencyStatus::Required => match (parsed_value, explicit_dep_activation) { + (FeatureValue::Feature(dep_name), false) + | (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key, + (FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => { + false + } + }, + } + .then(|| idx) + }) + .collect(); + + // Remove found idx in revers order so we don't invalidate the idx. + for idx in remove_list.iter().rev() { + feature_values.remove(*idx); + } + + if status == DependencyStatus::Required { + for value in feature_values.iter_mut() { + let parsed_value = if let Some(value) = value.as_str() { + FeatureValue::new(InternedString::new(value)) + } else { + continue; + }; + if let FeatureValue::DepFeature { + dep_name, + dep_feature, + weak, + } = parsed_value + { + if dep_name == dep_key && weak { + *value = format!("{dep_name}/{dep_feature}").into(); + } + } + } + } +} + +pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool { + item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false) +} + +fn parse_manifest_err() -> anyhow::Error { + anyhow::format_err!("unable to parse external Cargo.toml") +} + +fn non_existent_table_err(table: impl std::fmt::Display) -> anyhow::Error { + anyhow::format_err!("the table `{table}` could not be found.") +} diff --git a/src/cargo/ops/cargo_add/mod.rs b/src/cargo/ops/cargo_add/mod.rs new file mode 100644 index 000000000..99d0a91e1 --- /dev/null +++ b/src/cargo/ops/cargo_add/mod.rs @@ -0,0 +1,639 @@ +//! Core of cargo-add command + +mod crate_spec; +mod dependency; +mod manifest; + +use std::collections::BTreeSet; +use std::collections::VecDeque; +use std::path::Path; + +use cargo_util::paths; +use indexmap::IndexSet; +use toml_edit::Item as TomlItem; + +use crate::core::dependency::DepKind; +use crate::core::registry::PackageRegistry; +use crate::core::Package; +use crate::core::Registry; +use crate::core::Shell; +use crate::core::Workspace; +use crate::CargoResult; +use crate::Config; +use crate_spec::CrateSpec; +use dependency::Dependency; +use dependency::GitSource; +use dependency::PathSource; +use dependency::RegistrySource; +use dependency::Source; +use manifest::LocalManifest; + +pub use manifest::DepTable; + +/// Information on what dependencies should be added +#[derive(Clone, Debug)] +pub struct AddOptions<'a> { + /// Configuration information for cargo operations + pub config: &'a Config, + /// Package to add dependencies to + pub spec: &'a Package, + /// Dependencies to add or modify + pub dependencies: Vec, + /// Which dependency section to add these to + pub section: DepTable, + /// Act as if dependencies will be added + pub dry_run: bool, +} + +/// Add dependencies to a manifest +pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<()> { + let dep_table = options + .section + .to_table() + .into_iter() + .map(String::from) + .collect::>(); + + let manifest_path = options.spec.manifest_path().to_path_buf(); + let mut manifest = LocalManifest::try_new(&manifest_path)?; + let legacy = manifest.get_legacy_sections(); + if !legacy.is_empty() { + anyhow::bail!( + "Deprecated dependency sections are unsupported: {}", + legacy.join(", ") + ); + } + + let mut registry = PackageRegistry::new(options.config)?; + + let deps = { + let _lock = options.config.acquire_package_cache_lock()?; + registry.lock_patches(); + options + .dependencies + .iter() + .map(|raw| { + resolve_dependency( + &manifest, + raw, + workspace, + &options.section, + options.config, + &mut registry, + ) + }) + .collect::>>()? + }; + + let was_sorted = manifest + .get_table(&dep_table) + .map(TomlItem::as_table) + .map_or(true, |table_option| { + table_option.map_or(true, |table| is_sorted(table.iter().map(|(name, _)| name))) + }); + for dep in deps { + print_msg(&mut options.config.shell(), &dep, &dep_table)?; + if let Some(Source::Path(src)) = dep.source() { + if src.path == manifest.path.parent().unwrap_or_else(|| Path::new("")) { + anyhow::bail!( + "cannot add `{}` as a dependency to itself", + manifest.package_name()? + ) + } + } + if let Some(req_feats) = dep.features.as_ref() { + let req_feats: BTreeSet<_> = req_feats.iter().map(|s| s.as_str()).collect(); + + let available_features = dep + .available_features + .keys() + .map(|s| s.as_ref()) + .collect::>(); + + let mut unknown_features: Vec<&&str> = + req_feats.difference(&available_features).collect(); + unknown_features.sort(); + + if !unknown_features.is_empty() { + anyhow::bail!("unrecognized features: {unknown_features:?}"); + } + } + manifest.insert_into_table(&dep_table, &dep)?; + manifest.gc_dep(dep.toml_key()); + } + + if was_sorted { + if let Some(table) = manifest + .get_table_mut(&dep_table) + .ok() + .and_then(TomlItem::as_table_like_mut) + { + table.sort_values(); + } + } + + if options.dry_run { + options.config.shell().warn("aborting add due to dry run")?; + } else { + manifest.write()?; + } + + Ok(()) +} + +/// Dependency entry operation +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DepOp { + /// Describes the crate + pub crate_spec: Option, + /// Dependency key, overriding the package name in crate_spec + pub rename: Option, + + /// Feature flags to activate + pub features: Option>, + /// Whether the default feature should be activated + pub default_features: Option, + + /// Whether dependency is optional + pub optional: Option, + + /// Registry for looking up dependency version + pub registry: Option, + + /// Git repo for dependency + pub path: Option, + /// Git repo for dependency + pub git: Option, + /// Specify an alternative git branch + pub branch: Option, + /// Specify a specific git rev + pub rev: Option, + /// Specify a specific git tag + pub tag: Option, +} + +fn resolve_dependency( + manifest: &LocalManifest, + arg: &DepOp, + ws: &Workspace<'_>, + section: &DepTable, + config: &Config, + registry: &mut PackageRegistry<'_>, +) -> CargoResult { + let crate_spec = arg + .crate_spec + .as_deref() + .map(CrateSpec::resolve) + .transpose()?; + let mut selected_dep = if let Some(url) = &arg.git { + let mut src = GitSource::new(url); + if let Some(branch) = &arg.branch { + src = src.set_branch(branch); + } + if let Some(tag) = &arg.tag { + src = src.set_tag(tag); + } + if let Some(rev) = &arg.rev { + src = src.set_rev(rev); + } + + let selected = if let Some(crate_spec) = &crate_spec { + if let Some(v) = crate_spec.version_req() { + // crate specifier includes a version (e.g. `docopt@0.8`) + anyhow::bail!("cannot specify a git URL (`{url}`) with a version (`{v}`)."); + } + let dependency = crate_spec.to_dependency()?.set_source(src); + let selected = select_package(&dependency, config, registry)?; + if dependency.name != selected.name { + config.shell().warn(format!( + "translating `{}` to `{}`", + dependency.name, selected.name, + ))?; + } + selected + } else { + let mut source = crate::sources::GitSource::new(src.source_id()?, config)?; + let packages = source.read_packages()?; + let package = infer_package(packages, &src)?; + Dependency::from(package.summary()) + }; + selected + } else if let Some(raw_path) = &arg.path { + let path = paths::normalize_path(&std::env::current_dir()?.join(raw_path)); + let src = PathSource::new(&path); + + let selected = if let Some(crate_spec) = &crate_spec { + if let Some(v) = crate_spec.version_req() { + // crate specifier includes a version (e.g. `docopt@0.8`) + anyhow::bail!("cannot specify a path (`{raw_path}`) with a version (`{v}`)."); + } + let dependency = crate_spec.to_dependency()?.set_source(src); + let selected = select_package(&dependency, config, registry)?; + if dependency.name != selected.name { + config.shell().warn(format!( + "translating `{}` to `{}`", + dependency.name, selected.name, + ))?; + } + selected + } else { + let source = crate::sources::PathSource::new(&path, src.source_id()?, config); + let packages = source.read_packages()?; + let package = infer_package(packages, &src)?; + Dependency::from(package.summary()) + }; + selected + } else if let Some(crate_spec) = &crate_spec { + crate_spec.to_dependency()? + } else { + anyhow::bail!("dependency name is required"); + }; + selected_dep = populate_dependency(selected_dep, arg); + + let old_dep = get_existing_dependency(manifest, selected_dep.toml_key(), section)?; + let mut dependency = if let Some(mut old_dep) = old_dep.clone() { + if old_dep.name != selected_dep.name { + // Assuming most existing keys are not relevant when the package changes + if selected_dep.optional.is_none() { + selected_dep.optional = old_dep.optional; + } + selected_dep + } else { + if selected_dep.source().is_some() { + // Overwrite with `crate_spec` + old_dep.source = selected_dep.source; + } + old_dep = populate_dependency(old_dep, arg); + old_dep.available_features = selected_dep.available_features; + old_dep + } + } else { + selected_dep + }; + + if dependency.source().is_none() { + if let Some(package) = ws.members().find(|p| p.name().as_str() == dependency.name) { + // Only special-case workspaces when the user doesn't provide any extra + // information, otherwise, trust the user. + let mut src = PathSource::new(package.root()); + // dev-dependencies do not need the version populated + if section.kind() != DepKind::Development { + let op = ""; + let v = format!("{op}{version}", version = package.version()); + src = src.set_version(v); + } + dependency = dependency.set_source(src); + } else { + let latest = get_latest_dependency(&dependency, false, config, registry)?; + + if dependency.name != latest.name { + config.shell().warn(format!( + "translating `{}` to `{}`", + dependency.name, latest.name, + ))?; + dependency.name = latest.name; // Normalize the name + } + dependency = dependency + .set_source(latest.source.expect("latest always has a source")) + .set_available_features(latest.available_features); + } + } + + let version_required = dependency.source().and_then(|s| s.as_registry()).is_some(); + let version_optional_in_section = section.kind() == DepKind::Development; + let preserve_existing_version = old_dep + .as_ref() + .map(|d| d.version().is_some()) + .unwrap_or(false); + if !version_required && !preserve_existing_version && version_optional_in_section { + // dev-dependencies do not need the version populated + dependency = dependency.clear_version(); + } + + dependency = populate_available_features(dependency, config, registry)?; + + Ok(dependency) +} + +/// Provide the existing dependency for the target table +/// +/// If it doesn't exist but exists in another table, let's use that as most likely users +/// want to use the same version across all tables unless they are renaming. +fn get_existing_dependency( + manifest: &LocalManifest, + dep_key: &str, + section: &DepTable, +) -> CargoResult> { + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + enum Key { + Error, + Dev, + Build, + Normal, + Existing, + } + + let mut possible: Vec<_> = manifest + .get_dependency_versions(dep_key) + .map(|(path, dep)| { + let key = if path == *section { + (Key::Existing, true) + } else if dep.is_err() { + (Key::Error, path.target().is_some()) + } else { + let key = match path.kind() { + DepKind::Normal => Key::Normal, + DepKind::Build => Key::Build, + DepKind::Development => Key::Dev, + }; + (key, path.target().is_some()) + }; + (key, dep) + }) + .collect(); + possible.sort_by_key(|(key, _)| *key); + let (key, dep) = if let Some(item) = possible.pop() { + item + } else { + return Ok(None); + }; + let mut dep = dep?; + + if key.0 != Key::Existing { + // When the dep comes from a different section, we only care about the source and not any + // of the other fields, like `features` + let unrelated = dep; + dep = Dependency::new(&unrelated.name); + dep.source = unrelated.source.clone(); + dep.registry = unrelated.registry.clone(); + + // dev-dependencies do not need the version populated when path is set though we + // should preserve it if the user chose to populate it. + let version_required = unrelated.source().and_then(|s| s.as_registry()).is_some(); + let version_optional_in_section = section.kind() == DepKind::Development; + if !version_required && version_optional_in_section { + dep = dep.clear_version(); + } + } + + Ok(Some(dep)) +} + +fn get_latest_dependency( + dependency: &Dependency, + _flag_allow_prerelease: bool, + config: &Config, + registry: &mut PackageRegistry<'_>, +) -> CargoResult { + let query = dependency.query(config)?; + let possibilities = loop { + let fuzzy = true; + match registry.query_vec(&query, fuzzy) { + std::task::Poll::Ready(res) => { + break res?; + } + std::task::Poll::Pending => registry.block_until_ready()?, + } + }; + let latest = possibilities + .iter() + .max_by_key(|s| { + // Fallback to a pre-release if no official release is available by sorting them as + // less. + let stable = s.version().pre.is_empty(); + (stable, s.version()) + }) + .ok_or_else(|| { + anyhow::format_err!("the crate `{dependency}` could not be found in registry index.") + })?; + let mut dep = Dependency::from(latest); + if let Some(reg_name) = dependency.registry.as_deref() { + dep = dep.set_registry(reg_name); + } + Ok(dep) +} + +fn select_package( + dependency: &Dependency, + config: &Config, + registry: &mut PackageRegistry<'_>, +) -> CargoResult { + let query = dependency.query(config)?; + let possibilities = loop { + let fuzzy = false; // Returns all for path/git + match registry.query_vec(&query, fuzzy) { + std::task::Poll::Ready(res) => { + break res?; + } + std::task::Poll::Pending => registry.block_until_ready()?, + } + }; + match possibilities.len() { + 0 => { + let source = dependency + .source() + .expect("source should be resolved before here"); + anyhow::bail!("the crate `{dependency}` could not be found at `{source}`") + } + 1 => { + let mut dep = Dependency::from(&possibilities[0]); + if let Some(reg_name) = dependency.registry.as_deref() { + dep = dep.set_registry(reg_name); + } + Ok(dep) + } + _ => { + let source = dependency + .source() + .expect("source should be resolved before here"); + anyhow::bail!( + "unexpectedly found multiple copies of crate `{dependency}` at `{source}`" + ) + } + } +} + +fn infer_package(mut packages: Vec, src: &dyn std::fmt::Display) -> CargoResult { + let package = match packages.len() { + 0 => { + anyhow::bail!("no packages found at `{src}`"); + } + 1 => packages.pop().expect("match ensured element is present"), + _ => { + let mut names: Vec<_> = packages + .iter() + .map(|p| p.name().as_str().to_owned()) + .collect(); + names.sort_unstable(); + anyhow::bail!("multiple packages found at `{src}`: {}", names.join(", ")); + } + }; + Ok(package) +} + +fn populate_dependency(mut dependency: Dependency, arg: &DepOp) -> Dependency { + if let Some(registry) = &arg.registry { + if registry.is_empty() { + dependency.registry = None; + } else { + dependency.registry = Some(registry.to_owned()); + } + } + if let Some(value) = arg.optional { + if value { + dependency.optional = Some(true); + } else { + dependency.optional = None; + } + } + if let Some(value) = arg.default_features { + if value { + dependency.default_features = None; + } else { + dependency.default_features = Some(false); + } + } + if let Some(value) = arg.features.as_ref() { + dependency = dependency.extend_features(value.iter().cloned()); + } + + if let Some(rename) = &arg.rename { + dependency = dependency.set_rename(rename); + } + + dependency +} + +/// Lookup available features +fn populate_available_features( + mut dependency: Dependency, + config: &Config, + registry: &mut PackageRegistry<'_>, +) -> CargoResult { + if !dependency.available_features.is_empty() { + return Ok(dependency); + } + + let query = dependency.query(config)?; + let possibilities = loop { + match registry.query_vec(&query, true) { + std::task::Poll::Ready(res) => { + break res?; + } + std::task::Poll::Pending => registry.block_until_ready()?, + } + }; + // Ensure widest feature flag compatibility by picking the earliest version that could show up + // in the lock file for a given version requirement. + let lowest_common_denominator = possibilities + .iter() + .min_by_key(|s| { + // Fallback to a pre-release if no official release is available by sorting them as + // more. + let is_pre = !s.version().pre.is_empty(); + (is_pre, s.version()) + }) + .ok_or_else(|| { + anyhow::format_err!("the crate `{dependency}` could not be found in registry index.") + })?; + dependency = dependency.set_available_features_from_cargo(lowest_common_denominator.features()); + + Ok(dependency) +} + +fn print_msg(shell: &mut Shell, dep: &Dependency, section: &[String]) -> CargoResult<()> { + use std::fmt::Write; + + let mut message = String::new(); + write!(message, "{}", dep.name)?; + match dep.source() { + Some(Source::Registry(src)) => { + if src.version.chars().next().unwrap_or('0').is_ascii_digit() { + write!(message, " v{}", src.version)?; + } else { + write!(message, " {}", src.version)?; + } + } + Some(Source::Path(_)) => { + write!(message, " (local)")?; + } + Some(Source::Git(_)) => { + write!(message, " (git)")?; + } + None => {} + } + write!(message, " to")?; + if dep.optional().unwrap_or(false) { + write!(message, " optional")?; + } + let section = if section.len() == 1 { + section[0].clone() + } else { + format!("{} for target `{}`", §ion[2], §ion[1]) + }; + write!(message, " {section}")?; + write!(message, ".")?; + + let mut activated: IndexSet<_> = dep.features.iter().flatten().map(|s| s.as_str()).collect(); + if dep.default_features().unwrap_or(true) { + activated.insert("default"); + } + let mut walk: VecDeque<_> = activated.iter().cloned().collect(); + while let Some(next) = walk.pop_front() { + walk.extend( + dep.available_features + .get(next) + .into_iter() + .flatten() + .map(|s| s.as_str()), + ); + activated.extend( + dep.available_features + .get(next) + .into_iter() + .flatten() + .map(|s| s.as_str()), + ); + } + activated.remove("default"); + activated.sort(); + let mut deactivated = dep + .available_features + .keys() + .filter(|f| !activated.contains(f.as_str()) && *f != "default") + .collect::>(); + deactivated.sort(); + if !activated.is_empty() || !deactivated.is_empty() { + writeln!(message)?; + write!(message, "{:>13}Features:", " ")?; + for feat in activated { + writeln!(message)?; + write!(message, "{:>13}+ {}", " ", feat)?; + } + for feat in deactivated { + writeln!(message)?; + write!(message, "{:>13}- {}", " ", feat)?; + } + } + + shell.status("Adding", message)?; + + Ok(()) +} + +// Based on Iterator::is_sorted from nightly std; remove in favor of that when stabilized. +fn is_sorted(mut it: impl Iterator) -> bool { + let mut last = match it.next() { + Some(e) => e, + None => return true, + }; + + for curr in it { + if curr < last { + return false; + } + last = curr; + } + + true +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index e81486f3d..4e694ca6f 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -32,6 +32,7 @@ pub use self::resolve::{ }; pub use self::vendor::{vendor, VendorOptions}; +pub mod cargo_add; mod cargo_clean; mod cargo_compile; pub mod cargo_config;