Skip to content

Commit abed0d2

Browse files
committed
Auto merge of #11099 - cassaundra:cargo-remove, r=epage
Import `cargo remove` into cargo ## What does this PR try to resolve? This PR merges `cargo remove` from [cargo-edit](https://github.com/killercup/cargo-edit) into cargo. ### Motivation - General approval from community, see #5586 and #10520. - Satisfying symmetry between add and remove. - Help users clean up their manifests (for example, when users forget to remove optional dependencies from feature lists). With #10472, cargo-add was added to cargo. As part of that discussion, it was also proposed that `cargo rm` (now `cargo remove`) eventually be added as well. ### Drawbacks - Additional code always opens the door for more bugs and features - The scope of this command is fairly small though - Known bugs and most known features were resolved before this merge proposal ### Behavior `cargo remove` operates on one or more dependencies from a manifest, removing them from a specified dependencies section (using the same flags as `cargo-add`) and from `[features]` activations if the dependency is optional. Feature lists themselves are not automatically removed when made empty. Like with cargo-add, the lock file is automatically updated. Note: like `cargo add`, `cargo remove` refers to dependency names, rather than crate names, which can be different with the presence of the `name` field. Note: `cargo rm` has been renamed to `cargo remove`, based on prior art and user feedback (see [discussion](#10520)). Although this renaming is arguably an improvement, adding an `rm` alias could make the switch easier for existing users of cargo-edit (at the cost of a naming conflict which would merit insta-stabilization). #### Help output <details> ```shell $ cargo run -- remove --help cargo-remove Remove dependencies from a Cargo.toml manifest file USAGE: cargo remove [OPTIONS] <DEP_ID>... ARGS: <DEP_ID>... Dependencies to be removed OPTIONS: -p, --package [<SPEC>...] Package to remove from -v, --verbose Use verbose output (-vv very verbose/build.rs output) --manifest-path <PATH> Path to Cargo.toml --offline Run without accessing the network -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 SECTION: --dev Remove as development dependency --build Remove as build dependency --target <TARGET> Remove as dependency from the given target platform ``` </details> #### Example usage ``` cargo remove serde cargo remove criterion httpmock --dev cargo remove winhttp --target x86_64-pc-windows-gnu cargo remove --package core toml ``` ## How should we test and review this PR? This is following the pattern from cargo-add which was implemented in three different PRs (implementation, documentation, and completions), in the interest of reducing the focusing discussions in each PR and allowing cargo-add's behavior to settle to avoid documentation churn. 1. #10472 2. #10578 3. #10577 The remaining changes (documentation and shell completions) will follow shortly after. Some work has already begun on this feature in #11059. Work on this feature was carried out on the [`merge-rm`](killercup/cargo-edit@master...merge-rm) branch of cargo-edit with PRs reviewed by `@epage.` If you are interested in seeing how this feature evolved to better match cargo's internals, you might find the commit history there to be helpful. As this PR is reviewed, changes will be made both here and on that branch, with the commit history being fully maintained on the latter. `cargo remove` is structured like most other subcommands: - `src/bin/cargo/commands/remove.rs` contains the cli handling and top-level execution. - `src/cargo/ops/cargo_remove.rs` contains the implementation of the feature itself. In order to support this feature, the `remove_from_table` util was added to `util::toml_mut::manifest::LocalManifest`. Tests are split out into a separate commit to make it easier to review the production code and tests. Tests have been implemented with `snapbox`, structured similarly to the tests of `cargo add`. ### Prior art - Python: [`poetry remove`](https://python-poetry.org/docs/cli/#remove) - Supports dry run - JavaScript: [`yarn remove`](https://yarnpkg.com/cli/remove) - Supports wildcards - JavaScript: [`pnpm remove`](https://pnpm.io/cli/remove) - Go: [`go get`](https://go.dev/ref/mod#go-get) - `go get foo@none` to remove - Julia: [`pkg rm`](https://docs.julialang.org/en/v1/stdlib/Pkg/) - Supports `--all` to remove all dependencies - Ruby: [`bundle remove`](https://bundler.io/v2.2/man/bundle-remove.1.html) - Dart: [`dart pub remove`](https://dart.dev/tools/pub/cmd/pub-remove) - Supports dry run - Lua: [`luarocks remove`](https://github.com/luarocks/luarocks/wiki/remove) - Supports force remove - .NET: [`Uninstall-Package`](https://docs.microsoft.com/en-us/nuget/reference/ps-reference/ps-ref-uninstall-package) - Supports dry run - Supports removal of dependencies - Supports force remove (disregards dependencies) - Haxe: [`haxelib remove`](https://lib.haxe.org/documentation/using-haxelib/#remove) - Racket: [`raco pkg remove`](https://docs.racket-lang.org/pkg/cmdline.html#%28part._raco-pkg-remove%29) - Supports dry run - Supports force remove (disregards dependencies) - Supports demotion to weak dependency (sort of a corollary of force remove) ### Insta-stabilization In the discussion of `cargo add`'s stabilization story ([Zulip stream](https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/Stablizing.20cargo-add)), it was brought up that the feature might benefit from being insta-stabilized to avoid making the cargo-edit version of the binary hard to access. Since `cargo rm` (from cargo-edit) was renamed to `cargo remove` here, [such a conflict no longer exists](https://crates.io/search?q=cargo%20remove), so this is less of a concern. Since this feature is already has a had a long run of user testing in cargo-edit and doesn't have unsettled UI questions like cargo-add did, it might still be a candidate for insta-stabilization. ### Deferred work Necessary future work: - Add documentation. - Add shell completions. - Perform GC on workspace dependencies when they are no longer used (see #8415). - This is inspired by a feature from the RFC that was dropped (unused dependencies triggering a warning) - This was deferred out to avoid challenges with testing nightly features It was found in the review of `cargo add` that it was best to defer these first two items to focus the discussion and as there was still behavior churn during the review of cargo-add. ### Future Possibilities The following are features which we might want to add to `cargo remove` in the future: - Add a `cargo rm` alias to ease transition for current cargo-edit users - Automatically convert between dash and underscores in deps: killercup/cargo-edit#690 - Remove unused dependencies: killercup/cargo-edit#415 - Clean up caches: killercup/cargo-edit#647 ### Additional information Fixes #10520.
2 parents 0b84a35 + 805f424 commit abed0d2

File tree

153 files changed

+1916
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+1916
-1
lines changed

src/bin/cargo/commands/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub fn builtin() -> Vec<Command> {
2626
pkgid::cli(),
2727
publish::cli(),
2828
read_manifest::cli(),
29+
remove::cli(),
2930
report::cli(),
3031
run::cli(),
3132
rustc::cli(),
@@ -68,6 +69,7 @@ pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResu
6869
"pkgid" => pkgid::exec,
6970
"publish" => publish::exec,
7071
"read-manifest" => read_manifest::exec,
72+
"remove" => remove::exec,
7173
"report" => report::exec,
7274
"run" => run::exec,
7375
"rustc" => rustc::exec,
@@ -110,6 +112,7 @@ pub mod package;
110112
pub mod pkgid;
111113
pub mod publish;
112114
pub mod read_manifest;
115+
pub mod remove;
113116
pub mod report;
114117
pub mod run;
115118
pub mod rustc;

src/bin/cargo/commands/remove.rs

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use cargo::core::dependency::DepKind;
2+
use cargo::ops::cargo_remove::remove;
3+
use cargo::ops::cargo_remove::RemoveOptions;
4+
use cargo::ops::resolve_ws;
5+
use cargo::util::command_prelude::*;
6+
use cargo::util::toml_mut::manifest::DepTable;
7+
8+
pub fn cli() -> clap::Command {
9+
clap::Command::new("remove")
10+
// Subcommand aliases are handled in `aliased_command()`.
11+
// .alias("rm")
12+
.about("Remove dependencies from a Cargo.toml manifest file")
13+
.args([clap::Arg::new("dependencies")
14+
.action(clap::ArgAction::Append)
15+
.required(true)
16+
.num_args(1..)
17+
.value_name("DEP_ID")
18+
.help("Dependencies to be removed")])
19+
.arg_package("Package to remove from")
20+
.arg_manifest_path()
21+
.arg_quiet()
22+
.arg_dry_run("Don't actually write the manifest")
23+
.next_help_heading("SECTION")
24+
.args([
25+
clap::Arg::new("dev")
26+
.long("dev")
27+
.conflicts_with("build")
28+
.action(clap::ArgAction::SetTrue)
29+
.group("section")
30+
.help("Remove as development dependency"),
31+
clap::Arg::new("build")
32+
.long("build")
33+
.conflicts_with("dev")
34+
.action(clap::ArgAction::SetTrue)
35+
.group("section")
36+
.help("Remove as build dependency"),
37+
clap::Arg::new("target")
38+
.long("target")
39+
.num_args(1)
40+
.value_name("TARGET")
41+
.value_parser(clap::builder::NonEmptyStringValueParser::new())
42+
.help("Remove as dependency from the given target platform"),
43+
])
44+
}
45+
46+
pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
47+
let dry_run = args.dry_run();
48+
49+
let workspace = args.workspace(config)?;
50+
let packages = args.packages_from_flags()?;
51+
let packages = packages.get_packages(&workspace)?;
52+
let spec = match packages.len() {
53+
0 => {
54+
return Err(CliError::new(
55+
anyhow::format_err!("no packages selected. Please specify one with `-p <PKG_ID>`"),
56+
101,
57+
));
58+
}
59+
1 => packages[0],
60+
len => {
61+
return Err(CliError::new(
62+
anyhow::format_err!(
63+
"{len} packages selected. Please specify one with `-p <PKG_ID>`",
64+
),
65+
101,
66+
));
67+
}
68+
};
69+
70+
let dependencies = args
71+
.get_many::<String>("dependencies")
72+
.expect("required(true)")
73+
.cloned()
74+
.collect();
75+
76+
let section = parse_section(args);
77+
78+
let options = RemoveOptions {
79+
config,
80+
spec,
81+
dependencies,
82+
section,
83+
dry_run,
84+
};
85+
remove(&options)?;
86+
87+
if !dry_run {
88+
// Reload the workspace since we've changed dependencies
89+
let ws = args.workspace(config)?;
90+
resolve_ws(&ws)?;
91+
}
92+
93+
Ok(())
94+
}
95+
96+
fn parse_section(args: &ArgMatches) -> DepTable {
97+
let dev = args.flag("dev");
98+
let build = args.flag("build");
99+
100+
let kind = if dev {
101+
DepKind::Development
102+
} else if build {
103+
DepKind::Build
104+
} else {
105+
DepKind::Normal
106+
};
107+
108+
let mut table = DepTable::new().set_kind(kind);
109+
110+
if let Some(target) = args.get_one::<String>("target") {
111+
assert!(!target.is_empty(), "Target specification may not be empty");
112+
table = table.set_target(target);
113+
}
114+
115+
table
116+
}

src/bin/cargo/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ fn main() {
3939

4040
/// Table for defining the aliases which come builtin in `Cargo`.
4141
/// The contents are structured as: `(alias, aliased_command, description)`.
42-
const BUILTIN_ALIASES: [(&str, &str, &str); 5] = [
42+
const BUILTIN_ALIASES: [(&str, &str, &str); 6] = [
4343
("b", "build", "alias: build"),
4444
("c", "check", "alias: check"),
4545
("d", "doc", "alias: doc"),
4646
("r", "run", "alias: run"),
4747
("t", "test", "alias: test"),
48+
("rm", "remove", "alias: remove"),
4849
];
4950

5051
/// Function which contains the list of all of the builtin aliases and it's

src/cargo/ops/cargo_remove.rs

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Core of cargo-remove command
2+
3+
use crate::core::Package;
4+
use crate::util::toml_mut::manifest::DepTable;
5+
use crate::util::toml_mut::manifest::LocalManifest;
6+
use crate::CargoResult;
7+
use crate::Config;
8+
9+
/// Remove a dependency from a Cargo.toml manifest file.
10+
#[derive(Debug)]
11+
pub struct RemoveOptions<'a> {
12+
/// Configuration information for Cargo operations
13+
pub config: &'a Config,
14+
/// Package to remove dependencies from
15+
pub spec: &'a Package,
16+
/// Dependencies to remove
17+
pub dependencies: Vec<String>,
18+
/// Which dependency section to remove these from
19+
pub section: DepTable,
20+
/// Whether or not to actually write the manifest
21+
pub dry_run: bool,
22+
}
23+
24+
/// Remove dependencies from a manifest
25+
pub fn remove(options: &RemoveOptions<'_>) -> CargoResult<()> {
26+
let dep_table = options
27+
.section
28+
.to_table()
29+
.into_iter()
30+
.map(String::from)
31+
.collect::<Vec<_>>();
32+
33+
let manifest_path = options.spec.manifest_path().to_path_buf();
34+
let mut manifest = LocalManifest::try_new(&manifest_path)?;
35+
36+
for dep in &options.dependencies {
37+
let section = if dep_table.len() >= 3 {
38+
format!("{} for target `{}`", &dep_table[2], &dep_table[1])
39+
} else {
40+
dep_table[0].clone()
41+
};
42+
options
43+
.config
44+
.shell()
45+
.status("Removing", format!("{dep} from {section}"))?;
46+
47+
manifest.remove_from_table(&dep_table, dep)?;
48+
49+
// Now that we have removed the crate, if that was the last reference to that
50+
// crate, then we need to drop any explicitly activated features on
51+
// that crate.
52+
manifest.gc_dep(dep);
53+
}
54+
55+
if options.dry_run {
56+
options
57+
.config
58+
.shell()
59+
.warn("aborting remove due to dry run")?;
60+
} else {
61+
manifest.write()?;
62+
}
63+
64+
Ok(())
65+
}

src/cargo/ops/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ mod cargo_output_metadata;
4545
mod cargo_package;
4646
mod cargo_pkgid;
4747
mod cargo_read_manifest;
48+
pub mod cargo_remove;
4849
mod cargo_run;
4950
mod cargo_test;
5051
mod cargo_uninstall;

src/cargo/util/toml_mut/manifest.rs

+29
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,26 @@ impl LocalManifest {
368368
Ok(())
369369
}
370370

371+
/// Remove entry from a Cargo.toml.
372+
pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
373+
let parent_table = self.get_table_mut(table_path)?;
374+
375+
let dep = parent_table
376+
.get_mut(name)
377+
.filter(|t| !t.is_none())
378+
.ok_or_else(|| non_existent_dependency_err(name, table_path.join(".")))?;
379+
380+
// remove the dependency
381+
*dep = toml_edit::Item::None;
382+
383+
// remove table if empty
384+
if parent_table.as_table_like().unwrap().is_empty() {
385+
*parent_table = toml_edit::Item::None;
386+
}
387+
388+
Ok(())
389+
}
390+
371391
/// Remove references to `dep_key` if its no longer present.
372392
pub fn gc_dep(&mut self, dep_key: &str) {
373393
let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
@@ -504,6 +524,8 @@ fn fix_feature_activations(
504524
}
505525
}
506526
}
527+
528+
feature_values.fmt();
507529
}
508530

509531
pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
@@ -517,3 +539,10 @@ fn parse_manifest_err() -> anyhow::Error {
517539
fn non_existent_table_err(table: impl std::fmt::Display) -> anyhow::Error {
518540
anyhow::format_err!("the table `{table}` could not be found.")
519541
}
542+
543+
fn non_existent_dependency_err(
544+
name: impl std::fmt::Display,
545+
table: impl std::fmt::Display,
546+
) -> anyhow::Error {
547+
anyhow::format_err!("the dependency `{name}` could not be found in `{table}`.")
548+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../remove-basic.in/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
use crate::cargo_remove::init_registry;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
let project = Project::from_template(curr_dir!().join("in"));
12+
let project_root = project.root();
13+
let cwd = &project_root;
14+
15+
snapbox::cmd::Command::cargo_ui()
16+
.arg("remove")
17+
.args(["clippy"])
18+
.current_dir(cwd)
19+
.assert()
20+
.success()
21+
.stdout_matches_path(curr_dir!().join("stdout.log"))
22+
.stderr_matches_path(curr_dir!().join("stderr.log"));
23+
24+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "cargo-remove-test-fixture"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[build-dependencies]
10+
semver = "0.1.0"
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
toml = "0.1"
17+
18+
[dev-dependencies]
19+
regex = "0.1.1"
20+
serde = "1.0.90"
21+
22+
[features]
23+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing clippy from dependencies
2+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/avoid_empty_tables/stdout.log

Whitespace-only changes.

tests/testsuite/cargo_remove/build/in

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../remove-basic.in/
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
use crate::cargo_remove::init_registry;
7+
8+
#[cargo_test]
9+
fn case() {
10+
init_registry();
11+
let project = Project::from_template(curr_dir!().join("in"));
12+
let project_root = project.root();
13+
let cwd = &project_root;
14+
15+
snapbox::cmd::Command::cargo_ui()
16+
.arg("remove")
17+
.args(["--build", "semver"])
18+
.current_dir(cwd)
19+
.assert()
20+
.success()
21+
.stdout_matches_path(curr_dir!().join("stdout.log"))
22+
.stderr_matches_path(curr_dir!().join("stderr.log"));
23+
24+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "cargo-remove-test-fixture"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[dependencies]
10+
docopt = "0.6"
11+
rustc-serialize = "0.4"
12+
semver = "0.1"
13+
toml = "0.1"
14+
clippy = "0.4"
15+
16+
[dev-dependencies]
17+
regex = "0.1.1"
18+
serde = "1.0.90"
19+
20+
[features]
21+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing semver from build-dependencies
2+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/build/stdout.log

Whitespace-only changes.

tests/testsuite/cargo_remove/dev/in

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../remove-basic.in/

0 commit comments

Comments
 (0)