Skip to content

Commit 69828aa

Browse files
committed
Clean up workspace dependencies after cargo remove
1 parent b592ba4 commit 69828aa

File tree

21 files changed

+280
-0
lines changed

21 files changed

+280
-0
lines changed

src/bin/cargo/commands/remove.rs

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
7777

7878
let options = RemoveOptions {
7979
config,
80+
workspace: &workspace,
8081
spec,
8182
dependencies,
8283
section,

src/cargo/ops/cargo_remove.rs

+101
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Core of cargo-remove command
22
3+
use crate::core::Dependency;
34
use crate::core::Package;
5+
use crate::core::Workspace;
46
use crate::util::toml_mut::manifest::DepTable;
57
use crate::util::toml_mut::manifest::LocalManifest;
68
use crate::CargoResult;
@@ -11,6 +13,8 @@ use crate::Config;
1113
pub struct RemoveOptions<'a> {
1214
/// Configuration information for Cargo operations
1315
pub config: &'a Config,
16+
/// Workspace in which the operation is occurring.
17+
pub workspace: &'a Workspace<'a>,
1418
/// Package to remove dependencies from
1519
pub spec: &'a Package,
1620
/// Dependencies to remove
@@ -33,7 +37,18 @@ pub fn remove(options: &RemoveOptions<'_>) -> CargoResult<()> {
3337
let manifest_path = options.spec.manifest_path().to_path_buf();
3438
let mut manifest = LocalManifest::try_new(&manifest_path)?;
3539

40+
let is_root = options.spec.manifest_path() == options.workspace.root_manifest();
41+
let mut parent_manifest = if !is_root {
42+
let data = cargo_util::paths::read(options.workspace.root_manifest())?;
43+
let manifest: toml_edit::Document = data.parse()?;
44+
Some(manifest)
45+
} else {
46+
None
47+
};
48+
3649
for dep in &options.dependencies {
50+
let is_workspace_dep = dependency_is_workspace(dep, &dep_table, &manifest);
51+
3752
let section = if dep_table.len() >= 3 {
3853
format!("{} for target `{}`", &dep_table[2], &dep_table[1])
3954
} else {
@@ -50,6 +65,23 @@ pub fn remove(options: &RemoveOptions<'_>) -> CargoResult<()> {
5065
// crate, then we need to drop any explicitly activated features on
5166
// that crate.
5267
manifest.gc_dep(dep);
68+
69+
// If this was the last instance of a workspace dependency, remove it
70+
// from the workspace dependencies.
71+
if is_workspace_dep
72+
&& !dependency_in_workspace(dep, &options.section, options.spec, options.workspace)
73+
{
74+
let workspace_manifest = match &mut parent_manifest {
75+
Some(parent_manifest) => parent_manifest,
76+
None => &mut manifest.data,
77+
};
78+
remove_dependency_from_workspace(dep, workspace_manifest);
79+
80+
options
81+
.config
82+
.shell()
83+
.status("Removing", format!("{dep} from workspace dependencies"))?;
84+
}
5385
}
5486

5587
if options.dry_run {
@@ -59,7 +91,76 @@ pub fn remove(options: &RemoveOptions<'_>) -> CargoResult<()> {
5991
.warn("aborting remove due to dry run")?;
6092
} else {
6193
manifest.write()?;
94+
95+
if let Some(parent_manifest) = parent_manifest {
96+
cargo_util::paths::write(
97+
options.workspace.root_manifest(),
98+
parent_manifest.to_string().as_bytes(),
99+
)?;
100+
}
62101
}
63102

64103
Ok(())
65104
}
105+
106+
/// Get whether or not a dependency is marked as `workspace`.
107+
fn dependency_is_workspace(dep: &str, dep_table: &[String], manifest: &LocalManifest) -> bool {
108+
if let Ok(toml_edit::Item::Table(table)) = manifest.get_table(dep_table) {
109+
let value = table.get(dep).and_then(|i| i.get("workspace"));
110+
if let Some(toml_edit::Item::Value(value)) = value {
111+
return value.as_bool() == Some(true);
112+
}
113+
}
114+
115+
false
116+
}
117+
118+
/// Get whether or not a dependency is depended upon in a workspace.
119+
fn dependency_in_workspace(
120+
dep: &str,
121+
dep_table: &DepTable,
122+
exclude: &Package,
123+
workspace: &Workspace<'_>,
124+
) -> bool {
125+
workspace.members().any(|p| {
126+
let manifest = LocalManifest::try_new(p.manifest_path()).unwrap(); // TODO handle error
127+
p.dependencies()
128+
.iter()
129+
.filter(|d| d.package_name().as_str() == dep)
130+
.map(|d| (d, dependency_to_table(d)))
131+
// ignore the dep we could have just removed
132+
.filter(|(_, t)| !(p == exclude && t == dep_table))
133+
// only dependencies marked workspace
134+
.filter(|(d, t)| {
135+
// information about workspace dependencies is not preserved at
136+
// this stage, so we have to manually read the TOML file
137+
let dep_table = t
138+
.to_table()
139+
.into_iter()
140+
.map(String::from)
141+
.collect::<Vec<_>>();
142+
dependency_is_workspace(&d.package_name(), &dep_table, &manifest)
143+
})
144+
.next()
145+
.is_some()
146+
})
147+
}
148+
149+
fn dependency_to_table(dep: &Dependency) -> DepTable {
150+
let mut dep_table = DepTable::new().set_kind(dep.kind());
151+
if let Some(target) = dep.platform().map(|p| format!("{p}")) {
152+
dep_table = dep_table.set_target(target);
153+
}
154+
dep_table
155+
}
156+
157+
/// Remove a dependency from a virtual workspace.
158+
fn remove_dependency_from_workspace(dep: &str, virtual_manifest: &mut toml_edit::Document) {
159+
if let Some(toml_edit::Item::Table(table)) = virtual_manifest
160+
.get_mut("workspace")
161+
.and_then(|t| t.get_mut("dependencies"))
162+
{
163+
table.set_implicit(true);
164+
table.remove(dep);
165+
}
166+
}

tests/testsuite/cargo_remove/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ mod target;
2222
mod target_build;
2323
mod target_dev;
2424
mod update_lock_file;
25+
mod virtual_workspace;
26+
mod workspace;
2527

2628
fn init_registry() {
2729
cargo_test_support::registry::init();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[workspace]
2+
members = [ "my-package" ]
3+
4+
[workspace.dependencies]
5+
semver = "0.1.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "my-package"
3+
version = "0.1.0"
4+
5+
[[bin]]
6+
name = "main"
7+
path = "src/main.rs"
8+
9+
[build-dependencies]
10+
semver = { workspace = true }
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
toml = "0.1"
17+
clippy = "0.4"
18+
19+
[dev-dependencies]
20+
regex = "0.1.1"
21+
serde = "1.0.90"
22+
23+
[features]
24+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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(["--package", "my-package", "--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,2 @@
1+
[workspace]
2+
members = [ "my-package" ]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "my-package"
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 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Removing semver from build-dependencies
2+
Removing semver from workspace dependencies
3+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/virtual_workspace/stdout.log

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[workspace]
2+
members = [ "my-member" ]
3+
4+
[workspace.dependencies]
5+
semver = "0.1.0"
6+
7+
[package]
8+
name = "my-package"
9+
version = "0.1.0"
10+
11+
[[bin]]
12+
name = "main"
13+
path = "src/main.rs"
14+
15+
[build-dependencies]
16+
semver = { workspace = true }
17+
18+
[dependencies]
19+
docopt = "0.6"
20+
rustc-serialize = "0.4"
21+
semver = "0.1"
22+
toml = "0.1"
23+
clippy = "0.4"
24+
25+
[dev-dependencies]
26+
regex = "0.1.1"
27+
serde = "1.0.90"
28+
29+
[features]
30+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "my-member"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]

tests/testsuite/cargo_remove/workspace/in/my-member/src/main.rs

Whitespace-only changes.
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(["--package", "my-package", "--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,24 @@
1+
[workspace]
2+
members = [ "my-member" ]
3+
4+
[package]
5+
name = "my-package"
6+
version = "0.1.0"
7+
8+
[[bin]]
9+
name = "main"
10+
path = "src/main.rs"
11+
12+
[dependencies]
13+
docopt = "0.6"
14+
rustc-serialize = "0.4"
15+
semver = "0.1"
16+
toml = "0.1"
17+
clippy = "0.4"
18+
19+
[dev-dependencies]
20+
regex = "0.1.1"
21+
serde = "1.0.90"
22+
23+
[features]
24+
std = ["serde/std", "semver/std"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "my-member"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]

tests/testsuite/cargo_remove/workspace/out/my-member/src/main.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Removing semver from build-dependencies
2+
Removing semver from workspace dependencies
3+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/workspace/stdout.log

Whitespace-only changes.

0 commit comments

Comments
 (0)