Auto merge of #10472 - epage:add, r=ehuss

feat: Import cargo-add into cargo

### Motivation

The reasons I'm aware of are:
- Large interest, see #5586
- Make it easier to add a dependency when you don't care about the version (instead of having to find it or just using the major version if thats all you remember)
- Provide a guided experience, including
  - Catch or prevent errors earlier in the process
  - Bring the Manifest format documentation into the terminal via `cargo add --help`
  - Using `version` and `path` for `dependencies` but `path` only for `dev-dependencies` (see crate-ci/cargo-release#288 which led to killercup/cargo-edit#480)

### Drawbacks

1. This is another area of consideration for new RFCs, like rust-lang/rfcs#3143 (this PR supports it) or rust-lang/rfcs#2906 (implementing it will require updating `cargo-add`)

2. This is a high UX feature that will draw a lot of attention (ie Issue influx)

e.g.
- killercup/cargo-edit#521
- killercup/cargo-edit#126
- killercup/cargo-edit#217

We've tried to reduce the UX influx by focusing the scope to preserving semantic information (custom sort order, comments, etc) but being opinionated on syntax (style of strings, etc)

### Behavior

Help output
<details>

```console
$ cargo run -- add --help
cargo-add                                                                                                                                  [4/4594]
Add dependencies to a Cargo.toml manifest file

USAGE:
    cargo add [OPTIONS] <DEP>[`@<VERSION>]` ...
    cargo add [OPTIONS] --path <PATH> ...
    cargo add [OPTIONS] --git <URL> ...

ARGS:
    <DEP_ID>...    Reference to a package to add as a dependency

OPTIONS:
        --no-default-features     Disable the default features
        --default-features        Re-enable the default features
    -F, --features <FEATURES>     Space-separated list of features to add
        --optional                Mark the dependency as optional
    -v, --verbose                 Use verbose output (-vv very verbose/build.rs output)
        --no-optional             Mark the dependency as required
        --color <WHEN>            Coloring: auto, always, never
        --rename <NAME>           Rename the dependency
        --frozen                  Require Cargo.lock and cache are up to date
        --manifest-path <PATH>    Path to Cargo.toml
        --locked                  Require Cargo.lock is up to date
    -p, --package <SPEC>          Package to modify
        --offline                 Run without accessing the network
        --config <KEY=VALUE>      Override a configuration value (unstable)
    -q, --quiet                   Do not print cargo log messages
        --dry-run                 Don't actually write the manifest
    -Z <FLAG>                     Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for
                                  details
    -h, --help                    Print help information

SOURCE:
        --path <PATH>        Filesystem path to local crate to add
        --git <URI>          Git repository location
        --branch <BRANCH>    Git branch to download the crate from
        --tag <TAG>          Git tag to download the crate from
        --rev <REV>          Git reference to download the crate from
        --registry <NAME>    Package registry for this dependency

SECTION:
        --dev                Add as development dependency
        --build              Add as build dependency
        --target <TARGET>    Add as dependency to the given target platform

EXAMPLES:
  $ cargo add regex --build
  $ cargo add trycmd --dev
  $ cargo add --path ./crate/parser/
  $ cargo add serde serde_json -F serde/derive
```

</details>

Example commands
```rust
cargo add regex
cargo add regex serde
cargo add regex@1
cargo add regex@~1.0
cargo add --path ../dependency
```
For an exhaustive set of examples, see [tests](https://github.com/killercup/cargo-edit/blob/merge-add/crates/cargo-add/tests/testsuite/cargo_add.rs) and associated snapshots

Particular points
- Effectively there are two modes
  - Fill in any relevant field for one package
  - Add multiple packages, erroring for fields that are package-specific (`--rename`)
  - Note that `--git` and `--path` only accept multiple packages from that one source
- We infer if the `dependencies` table is sorted and preserve that sorting when adding a new dependency
- Adding a workspace dependency
  - dev-dependencies always use path
  - all other dependencies use version + path
- Behavior is idempotent, allowing you to run `cargo add serde serde_json -F serde/derive` safely if you already had a dependency on `serde` but without `serde_json`
- When a registry dependency's version req is unspecified, we'll first reuse the version req from another dependency section in the manifest.  If that doesn't exist, we'll use the latest version in the registry as the version req

### Additional decisions

Accepting the proposed `cargo-add` as-is assumes the acceptance of the following:
- Add the `-F` short-hand for `--features` to all relevant cargo commands
- Support ``@`` in pkgids in other commands where we accept `:`
- Add support for `<name>`@<version>`` in more commands, like `cargo yank` and `cargo install`

### Alternatives

- Use `:` instead of ``@`` for versions
- Flags like `--features`, `--optional`, `--no-default-features` would be position-sensitive, ie they would only apply to the crate immediate preceding them
  - This removes the dual-mode nature of the command and remove the need for the `+feature` syntax (`cargo add serde -F derive serde_json`)
  - There was concern over the rarity of position-sensitive flags in CLIs for adopting it here
- Support a `--sort` flag to sort the dependencies (existed previously)
  - To keep the scope small, we didn't want general manifest editing capabilities
- `--upgrade <POLICY>` flag to choose constraint (existed previously)
  - The flag was confusing as-is and we feel we should instead encourage people towards `^`
- `--allow-prerelease` so a `cargo add clap` can choose among pre-releases as well
  - We felt the pre-release story is too weak in cargo-generally atm for making it first class in `cargo-add`
- Offer `cargo add serde +derive serde_json` as a shorthand
- Infer path from a positional argument

### Prior Art

- *(Python)* [poetry add](https://python-poetry.org/docs/cli/#add)
  - `git+` is needed for inferring git dependencies, no separate  `--git` flags
  - git branch is specified via a URL fragment, instead of a `--branch`
- *(Javascript)* [yarn add](https://yarnpkg.com/cli/add)
  - `name@data` where data can be version, git (with fragment for branch), etc
  - `-E` / `--exact`, `-T` / `--tilde`, `-C` / `--caret` to control version requirement operator instead of `--upgrade <policy>` (also controlled through `defaultSemverRangePrefix` in config)
  - `--cached` for using the lock file (killercup/cargo-edit#41)
  - In addition to `--dev`, it has `--prefer-dev` which will only add the dependency if it doesn't already exist in `dependencies` as well as `dev-dependencies`
  - `--mode update-lockfile` will ensure the lock file gets updated as well
- *(Javascript)* [pnpm-add](https://pnpm.io/cli/add)
- *(Javascript)* npm doesn't have a native solution
  - Specify version with ``@<version>``
  - Also overloads `<name>[`@<version>]`` with path and repo
    - Supports a git host-specific protocol for shorthand, like `github:user/repo`
    - Uses fragment for git ref, seems to have some kind of special semver syntax for tags?
  - Only supports `--save-exact` / `-E` for operators outside of the default
- *(Go)* [go get](https://go.dev/ref/mod#go-get)
  - Specify version with ``@<version>``
  - Remove dependency with ``@none``
- *(Haskell)* stack doesn't seem to have a native solution
- *(Julia)* [pkg Add](https://docs.julialang.org/en/v1/stdlib/Pkg/)
- *(Ruby)* [bundle add](https://bundler.io/v2.2/man/bundle-add.1.html)
  - Uses `--version` / `-v` instead of `--vers` (we use `--vers` because of `--version` / `-V`)
  - `--source` instead of `path` (`path` correlates to manifest field)
  - Uses `--git` / `--branch` like `cargo-add`
- *(Dart)* [pub add](https://dart.dev/tools/pub/cmd/pub-add)
  - Uses `--git-url` instead of `--git`
  - Uses `--git-ref` instead of `--branch`, `--tag`, `--rev`

### Future Possibilities

- Update lock file accordingly
- Exploring the idea of a [`--local` flag](https://github.com/killercup/cargo-edit/issues/590)
- Take the MSRV into account when automatically creating version req (https://github.com/killercup/cargo-edit/issues/587)
- Integrate rustsec to report advisories on new dependencies (https://github.com/killercup/cargo-edit/issues/512)
- Integrate with licensing to report license, block add, etc (e.g. https://github.com/killercup/cargo-edit/issues/386)
- Pull version from lock file (https://github.com/killercup/cargo-edit/issues/41)
- Exploring if any vendoring integration would be beneficial (currently errors)
- Upstream `cargo-rm` (#10520), `cargo-upgrade` (#10498), and `cargo-set-version` (in that order of priority)
- Update crates.io with `cargo add` snippets in addition to or replacing the manifest snippets

For more, see https://github.com/killercup/cargo-edit/issues?q=is%3Aissue+is%3Aopen+label%3Acargo-add

### How should we test and review this PR?

This is intentionally broken up into several commits to help reviewing
1. Import of production code from cargo-edit's `merge-add` branch, with only changes made to let it compile (e.g. fixing up of `use` statements).
2. Import of test code / snapshots.  The only changes outside of the import were to add the `snapbox` dev-dependency and to `mod cargo_add` into the testsuite
3. This extends the work in #10425 so I could add back in the color highlighting I had to remove as part of switching `cargo-add` from direct termcolor calls to calling into `Shell`

Structure-wise, this is similar to other commands
- `bin` only defines a CLI and adapts it to an `AddOptions`
- `ops` contains a focused API with everything buried under it

The "op" contains a directory, instead of just a file, because of the amount of content.  Currently, all editing code is contained in there.  Most of this will be broken out and reused when other `cargo-edit` commands are added but holding off on that for now to separate out the editing API discussions from just getting the command in.

Within the github UI, I'd recommend looking at individual commits (and the `merge-add` branch if interested), skipping commit 2.  Commit 2 would be easier to browse locally.

`cargo-add` is mostly covered by end-to-end tests written using `snapbox`, including error cases.

There is additional cleanup that would ideally happen that was excluded intentionally from this PR to keep it better scoped, including
- Consolidating environment variables for end-to-end tests of `cargo`
- Pulling out the editing API, as previously mentioned
  - Where the editing API should live (`cargo::edit`?)
  - Any more specific naming of types to reduce clashes (e.g. `Dependency` or `Manifest` being fairly generic).
- Possibly sharing `SourceId` creation between `cargo install` and `cargo edit`
- Explore using `snapbox` in more of cargo's tests

Implementation justifications:
- `dunce` and `pathdiff` dependencies: needed for taking paths relative to the user and make them relative to the manifest being edited
- `indexmap` dependency (already a transitive dependency): Useful for preserving uniqueness while preserving order, like with feature values
- `snapbox` dev-dependency: Originally it was used to make it easy to update tests as the UX changed in prep for merging but it had the added benefit of making some UX bugs easier to notice so they got fixed.  Overall, I'd like to see it become the cargo-agnostic version of `cargo-test-support` so there is a larger impact when improvements are made
- `parse_feature` function: `CliFeatures` forces items through a `BTreeSet`, losing the users specified order which we wanted to preserve.

### Additional Information

See also [the internals thread](https://internals.rust-lang.org/t/feedback-on-cargo-add-before-its-merged/16024).

Fixes #5586
This commit is contained in:
bors 2022-04-18 18:26:12 +00:00
commit 6a4d98d232
500 changed files with 6546 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"] }
@ -99,6 +100,7 @@ features = [
[dev-dependencies]
cargo-test-macro = { path = "crates/cargo-test-macro" }
cargo-test-support = { path = "crates/cargo-test-support" }
snapbox = { version = "0.2.8", features = ["diff", "path"] }
[build-dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }

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

@ -344,6 +344,17 @@ impl Shell {
self.output.write_stdout(fragment, color)
}
/// Write a styled fragment
///
/// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
pub fn write_stderr(
&mut self,
fragment: impl fmt::Display,
color: &ColorSpec,
) -> CargoResult<()> {
self.output.write_stderr(fragment, color)
}
/// Prints a message to stderr and translates ANSI escape code into console colors.
pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> {
if self.needs_clear {
@ -450,6 +461,22 @@ impl ShellOut {
Ok(())
}
/// Write a styled fragment
fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> CargoResult<()> {
match *self {
ShellOut::Stream { ref mut stderr, .. } => {
stderr.reset()?;
stderr.set_color(&color)?;
write!(stderr, "{}", fragment)?;
stderr.reset()?;
}
ShellOut::Write(ref mut w) => {
write!(w, "{}", fragment)?;
}
}
Ok(())
}
/// Gets stdout as a `io::Write`.
fn stdout(&mut self) -> &mut dyn Write {
match *self {

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,647 @@
//! 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 termcolor::Color::Green;
use termcolor::Color::Red;
use termcolor::ColorSpec;
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;
if matches!(shell.verbosity(), crate::core::shell::Verbosity::Quiet) {
return Ok(());
}
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, ".")?;
shell.status("Adding", 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() {
let prefix = format!("{:>13}", " ");
shell.write_stderr(format_args!("{}Features:\n", prefix), &ColorSpec::new())?;
for feat in activated {
shell.write_stderr(&prefix, &ColorSpec::new())?;
shell.write_stderr('+', &ColorSpec::new().set_bold(true).set_fg(Some(Green)))?;
shell.write_stderr(format_args!(" {}\n", feat), &ColorSpec::new())?;
}
for feat in deactivated {
shell.write_stderr(&prefix, &ColorSpec::new())?;
shell.write_stderr('-', &ColorSpec::new().set_bold(true).set_fg(Some(Red)))?;
shell.write_stderr(format_args!(" {}\n", feat), &ColorSpec::new())?;
}
}
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;

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
my-package = "99999.0.0"

View File

@ -0,0 +1,2 @@
Updating `dummy-registry` index
Adding my-package v99999.0.0 to dependencies.

View File

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,9 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
my-package1 = "99999.0.0"
my-package2 = "99999.0.0"

View File

@ -0,0 +1,3 @@
Updating `dummy-registry` index
Adding my-package1 v99999.0.0 to dependencies.
Adding my-package2 v99999.0.0 to dependencies.

View File

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,9 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
inflector = "0.11.4"
linked-hash-map = "0.5.4"

View File

@ -0,0 +1,18 @@
Updating `dummy-registry` index
warning: translating `linked_hash_map` to `linked-hash-map`
warning: translating `Inflector` to `inflector`
Adding linked-hash-map v0.5.4 to dependencies.
Features:
- clippy
- heapsize
- heapsize_impl
- nightly
- serde
- serde_impl
- serde_test
Adding inflector v0.11.4 to dependencies.
Features:
+ heavyweight
+ lazy_static
+ regex
- unstable

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,9 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[build-dependencies]
my-build-package1 = "99999.0.0"
my-build-package2 = "99999.0.0"

View File

@ -0,0 +1,3 @@
Updating `dummy-registry` index
Adding my-build-package1 v99999.0.0 to build-dependencies.
Adding my-build-package2 v99999.0.0 to build-dependencies.

View File

View File

@ -0,0 +1,9 @@
[workspace]
exclude = ["dependency"]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
cargo-list-test-fixture-dependency = { version = "0.0.0", path = "dependency", optional = true, default-features = false, features = ["one", "two"], registry = "alternative" }

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture-dependency"
version = "0.0.0"

View File

@ -0,0 +1,12 @@
[workspace]
exclude = ["dependency"]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
cargo-list-test-fixture-dependency = { version = "0.0.0", path = "dependency", optional = true, default-features = false, features = ["one", "two"], registry = "alternative" }
[build-dependencies]
cargo-list-test-fixture-dependency = { version = "0.0.0", path = "dependency", registry = "alternative" }

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture-dependency"
version = "0.0.0"

View File

@ -0,0 +1 @@
Adding cargo-list-test-fixture-dependency (local) to build-dependencies.

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
some-package = { package = "my-package1", version = "0.1.1", optional = true }

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
some-package = { package = "my-package2", version = "99999.0.0", optional = true }

View File

@ -0,0 +1,2 @@
Updating `dummy-registry` index
Adding my-package2 v99999.0.0 to optional dependencies.

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,9 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
my-package1 = "99999.0.0"
my-package2 = "0.4.1"

View File

@ -0,0 +1,3 @@
Updating `dummy-registry` index
Adding my-package1 v99999.0.0 to dependencies.
Adding my-package2 v0.4.1 to dependencies.

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
my-package = { version = "99999.0.0", default_features = false }

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
my-package = { version = "99999.0.0", default_features = false }

View File

@ -0,0 +1 @@
error: Use of `default_features` in `my-package` is unsupported, please switch to `default-features`

View File

@ -0,0 +1,11 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dev_dependencies]
my-package = "99999.0.0"
[build_dependencies]
my-package = "99999.0.0"

View File

@ -0,0 +1,11 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dev_dependencies]
my-package = "99999.0.0"
[build_dependencies]
my-package = "99999.0.0"

View File

@ -0,0 +1 @@
error: Deprecated dependency sections are unsupported: dev_dependencies, build_dependencies

1
tests/snapshots/add/dev.in Symbolic link
View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,9 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dev-dependencies]
my-dev-package1 = "99999.0.0"
my-dev-package2 = "99999.0.0"

View File

@ -0,0 +1,3 @@
Updating `dummy-registry` index
Adding my-dev-package1 v99999.0.0 to dev-dependencies.
Adding my-dev-package2 v99999.0.0 to dev-dependencies.

View File

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"

View File

@ -0,0 +1,8 @@
error: The argument '--dev' cannot be used with '--build'
USAGE:
cargo add [OPTIONS] <DEP>[@<VERSION>] ...
cargo add [OPTIONS] --path <PATH> ...
cargo add [OPTIONS] --git <URL> ...
For more information try --help

View File

@ -0,0 +1,9 @@
[workspace]
exclude = ["dependency"]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
cargo-list-test-fixture-dependency = { version = "0.0.0", path = "dependency", optional = true, default-features = false, features = ["one", "two"], registry = "alternative" }

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture-dependency"
version = "0.0.0"

View File

@ -0,0 +1,12 @@
[workspace]
exclude = ["dependency"]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
cargo-list-test-fixture-dependency = { version = "0.0.0", path = "dependency", optional = true, default-features = false, features = ["one", "two"], registry = "alternative" }
[dev-dependencies]
cargo-list-test-fixture-dependency = { path = "dependency" }

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture-dependency"
version = "0.0.0"

View File

@ -0,0 +1 @@
Adding cargo-list-test-fixture-dependency (local) to dev-dependencies.

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"

View File

@ -0,0 +1,3 @@
Updating `dummy-registry` index
Adding my-package v99999.0.0 to dependencies.
warning: aborting add due to dry run

View File

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = { version = "99999.0.0", features = ["eyes"] }

View File

@ -0,0 +1,7 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
+ eyes
- ears
- mouth
- nose

View File

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = "99999.0.0"

View File

@ -0,0 +1,7 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
- ears
- eyes
- mouth
- nose

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = { version = "99999.0.0", features = ["eyes", "nose"] }

View File

@ -0,0 +1,7 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
+ eyes
+ nose
- ears
- mouth

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = { version = "99999.0.0", features = ["eyes"] }

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = { version = "99999.0.0", features = ["eyes"] }

View File

@ -0,0 +1,7 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
+ eyes
- ears
- mouth
- nose

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,8 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"
[dependencies]
your-face = { version = "99999.0.0", features = ["eyes", "nose"] }

View File

@ -0,0 +1,7 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
+ eyes
+ nose
- ears
- mouth

View File

@ -0,0 +1 @@
add-basic.in/

View File

@ -0,0 +1,5 @@
[workspace]
[package]
name = "cargo-list-test-fixture"
version = "0.0.0"

View File

@ -0,0 +1,9 @@
Updating `dummy-registry` index
Adding your-face v99999.0.0 to dependencies.
Features:
+ noze
- ears
- eyes
- mouth
- nose
error: unrecognized features: ["noze"]

1
tests/snapshots/add/git.in Symbolic link
View File

@ -0,0 +1 @@
add-basic.in/

Some files were not shown because too many files have changed in this diff Show More