mirror of
https://github.com/rust-lang/cargo
synced 2024-08-28 11:50:23 +00:00
feat: Add add
cargo subcommand
This is a fork of https://github.com/killercup/cargo-edit/tree/merge-add at d561719161ed5564111ff2152ff206463ec24cef
This commit is contained in:
parent
1073915930
commit
5ca5f2f157
|
@ -35,6 +35,7 @@ glob = "0.3.0"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
home = "0.5"
|
home = "0.5"
|
||||||
humantime = "2.0.0"
|
humantime = "2.0.0"
|
||||||
|
indexmap = "1"
|
||||||
ignore = "0.4.7"
|
ignore = "0.4.7"
|
||||||
lazy_static = "1.2.0"
|
lazy_static = "1.2.0"
|
||||||
jobserver = "0.1.24"
|
jobserver = "0.1.24"
|
||||||
|
@ -45,7 +46,7 @@ libgit2-sys = "0.13.2"
|
||||||
memchr = "2.1.3"
|
memchr = "2.1.3"
|
||||||
opener = "0.5"
|
opener = "0.5"
|
||||||
os_info = "3.0.7"
|
os_info = "3.0.7"
|
||||||
pathdiff = "0.2.1"
|
pathdiff = "0.2"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
rustfix = "0.6.0"
|
rustfix = "0.6.0"
|
||||||
semver = { version = "1.0.3", features = ["serde"] }
|
semver = { version = "1.0.3", features = ["serde"] }
|
||||||
|
|
360
src/bin/cargo/commands/add.rs
Normal file
360
src/bin/cargo/commands/add.rs
Normal file
|
@ -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] <DEP>[@<VERSION>] ...
|
||||||
|
cargo add [OPTIONS] --path <PATH> ...
|
||||||
|
cargo add [OPTIONS] --git <URL> ..."
|
||||||
|
)
|
||||||
|
.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:
|
||||||
|
- `<name>`, like `cargo add serde` (latest version will be used)
|
||||||
|
- `<name>@<version-req>`, 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 <PKGID>`"),
|
||||||
|
101,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
1 => packages[0],
|
||||||
|
len => {
|
||||||
|
return Err(CliError::new(
|
||||||
|
anyhow::format_err!(
|
||||||
|
"{len} packages selected. Please specify one with `-p <PKGID>`",
|
||||||
|
),
|
||||||
|
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<Vec<DepOp>> {
|
||||||
|
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::<IndexMap<_, _>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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<DepOp> = 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<bool> {
|
||||||
|
resolve_bool_arg(
|
||||||
|
matches.is_present("default-features"),
|
||||||
|
matches.is_present("no-default-features"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optional(matches: &ArgMatches) -> Option<bool> {
|
||||||
|
resolve_bool_arg(
|
||||||
|
matches.is_present("optional"),
|
||||||
|
matches.is_present("no-optional"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
|
||||||
|
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<Item = &str> {
|
||||||
|
// 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())
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ use crate::command_prelude::*;
|
||||||
|
|
||||||
pub fn builtin() -> Vec<App> {
|
pub fn builtin() -> Vec<App> {
|
||||||
vec![
|
vec![
|
||||||
|
add::cli(),
|
||||||
bench::cli(),
|
bench::cli(),
|
||||||
build::cli(),
|
build::cli(),
|
||||||
check::cli(),
|
check::cli(),
|
||||||
|
@ -42,6 +43,7 @@ pub fn builtin() -> Vec<App> {
|
||||||
|
|
||||||
pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResult> {
|
pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResult> {
|
||||||
let f = match cmd {
|
let f = match cmd {
|
||||||
|
"add" => add::exec,
|
||||||
"bench" => bench::exec,
|
"bench" => bench::exec,
|
||||||
"build" => build::exec,
|
"build" => build::exec,
|
||||||
"check" => check::exec,
|
"check" => check::exec,
|
||||||
|
@ -82,6 +84,7 @@ pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResu
|
||||||
Some(f)
|
Some(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod add;
|
||||||
pub mod bench;
|
pub mod bench;
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod check;
|
pub mod check;
|
||||||
|
|
63
src/cargo/ops/cargo_add/crate_spec.rs
Normal file
63
src/cargo/ops/cargo_add/crate_spec.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrateSpec {
|
||||||
|
/// Convert a string to a `Crate`
|
||||||
|
pub fn resolve(pkg_id: &str) -> CargoResult<Self> {
|
||||||
|
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<Dependency> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
1038
src/cargo/ops/cargo_add/dependency.rs
Normal file
1038
src/cargo/ops/cargo_add/dependency.rs
Normal file
File diff suppressed because it is too large
Load diff
507
src/cargo/ops/cargo_add/manifest.rs
Normal file
507
src/cargo/ops/cargo_add/manifest.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>) -> 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<DepKind> 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.<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<String> {
|
||||||
|
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.<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<Self, Self::Err> {
|
||||||
|
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<Self> {
|
||||||
|
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<Item = (DepTable, CargoResult<Dependency>)> + '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::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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<usize> = 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.")
|
||||||
|
}
|
639
src/cargo/ops/cargo_add/mod.rs
Normal file
639
src/cargo/ops/cargo_add/mod.rs
Normal file
|
@ -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<DepOp>,
|
||||||
|
/// 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::<Vec<_>>();
|
||||||
|
|
||||||
|
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::<CargoResult<Vec<_>>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
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::<BTreeSet<&str>>();
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
/// Dependency key, overriding the package name in crate_spec
|
||||||
|
pub rename: Option<String>,
|
||||||
|
|
||||||
|
/// Feature flags to activate
|
||||||
|
pub features: Option<IndexSet<String>>,
|
||||||
|
/// Whether the default feature should be activated
|
||||||
|
pub default_features: Option<bool>,
|
||||||
|
|
||||||
|
/// Whether dependency is optional
|
||||||
|
pub optional: Option<bool>,
|
||||||
|
|
||||||
|
/// Registry for looking up dependency version
|
||||||
|
pub registry: Option<String>,
|
||||||
|
|
||||||
|
/// Git repo for dependency
|
||||||
|
pub path: Option<String>,
|
||||||
|
/// Git repo for dependency
|
||||||
|
pub git: Option<String>,
|
||||||
|
/// Specify an alternative git branch
|
||||||
|
pub branch: Option<String>,
|
||||||
|
/// Specify a specific git rev
|
||||||
|
pub rev: Option<String>,
|
||||||
|
/// Specify a specific git tag
|
||||||
|
pub tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_dependency(
|
||||||
|
manifest: &LocalManifest,
|
||||||
|
arg: &DepOp,
|
||||||
|
ws: &Workspace<'_>,
|
||||||
|
section: &DepTable,
|
||||||
|
config: &Config,
|
||||||
|
registry: &mut PackageRegistry<'_>,
|
||||||
|
) -> CargoResult<Dependency> {
|
||||||
|
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<Option<Dependency>> {
|
||||||
|
#[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<Dependency> {
|
||||||
|
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<Dependency> {
|
||||||
|
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<Package>, src: &dyn std::fmt::Display) -> CargoResult<Package> {
|
||||||
|
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<Dependency> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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<Item = impl PartialOrd>) -> 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
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ pub use self::resolve::{
|
||||||
};
|
};
|
||||||
pub use self::vendor::{vendor, VendorOptions};
|
pub use self::vendor::{vendor, VendorOptions};
|
||||||
|
|
||||||
|
pub mod cargo_add;
|
||||||
mod cargo_clean;
|
mod cargo_clean;
|
||||||
mod cargo_compile;
|
mod cargo_compile;
|
||||||
pub mod cargo_config;
|
pub mod cargo_config;
|
||||||
|
|
Loading…
Reference in a new issue