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:
Ed Page 2022-03-10 11:20:50 -06:00
parent 1073915930
commit 5ca5f2f157
8 changed files with 2613 additions and 1 deletions

View File

@ -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"] }

View 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())
}

View File

@ -2,6 +2,7 @@ use crate::command_prelude::*;
pub fn builtin() -> Vec<App> {
vec![
add::cli(),
bench::cli(),
build::cli(),
check::cli(),
@ -42,6 +43,7 @@ pub fn builtin() -> Vec<App> {
pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> 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<fn(&mut Config, &ArgMatches) -> CliResu
Some(f)
}
pub mod add;
pub mod bench;
pub mod build;
pub mod check;

View 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()
}
}

File diff suppressed because it is too large Load Diff

View 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.")
}

View 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 `{}`", &section[2], &section[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
}

View File

@ -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;