Skip to content

Commit 43689d3

Browse files
committed
Auto merge of #11194 - cassaundra:cargo-remove-gc, r=epage
Clean profile, patch, and replace in cargo remove ### What does this PR try to resolve? This PR is part of the continued work on cargo remove (#11099, see deferred work). After a successful removal of a dependency, clean up the profile, patch, and replace sections to remove all references to it. **Note** the GC process was expanded to clean up not just references to the dependencies just removed, but also references of all dependencies. This was because there's not an easy way to determine which dependencies correspond to the given TOML keys, without either 1) figuring that out before the removal (therefore having to predict the behavior), or 2) returning that information from the remove function (somewhat unorthodox for an op). ### How should we review and test this PR? Verify that the implementation makes sense and that the tests are sufficient.
2 parents fa85418 + 299252c commit 43689d3

File tree

22 files changed

+463
-30
lines changed

22 files changed

+463
-30
lines changed

src/bin/cargo/commands/remove.rs

+193-30
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use cargo::core::dependency::DepKind;
2+
use cargo::core::PackageIdSpec;
23
use cargo::core::Workspace;
34
use cargo::ops::cargo_remove::remove;
45
use cargo::ops::cargo_remove::RemoveOptions;
56
use cargo::ops::resolve_ws;
67
use cargo::util::command_prelude::*;
78
use cargo::util::print_available_packages;
9+
use cargo::util::toml_mut::dependency::Dependency;
10+
use cargo::util::toml_mut::dependency::MaybeWorkspace;
11+
use cargo::util::toml_mut::dependency::Source;
812
use cargo::util::toml_mut::manifest::DepTable;
913
use cargo::util::toml_mut::manifest::LocalManifest;
1014
use cargo::CargoResult;
@@ -86,7 +90,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
8690
.get_many::<String>("dependencies")
8791
.expect("required(true)")
8892
.cloned()
89-
.collect();
93+
.collect::<Vec<_>>();
9094

9195
let section = parse_section(args);
9296

@@ -100,8 +104,8 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
100104
remove(&options)?;
101105

102106
if !dry_run {
103-
// Clean up workspace dependencies
104-
gc_workspace(&workspace, &options.dependencies)?;
107+
// Clean up the workspace
108+
gc_workspace(&workspace)?;
105109

106110
// Reload the workspace since we've changed dependencies
107111
let ws = args.workspace(config)?;
@@ -133,49 +137,208 @@ fn parse_section(args: &ArgMatches) -> DepTable {
133137
table
134138
}
135139

136-
/// Clean up workspace dependencies which no longer have a reference to them.
137-
fn gc_workspace(workspace: &Workspace<'_>, dependencies: &[String]) -> CargoResult<()> {
140+
/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest
141+
/// by removing dependencies which no longer have a reference to them.
142+
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
138143
let mut manifest: toml_edit::Document =
139144
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
145+
let mut is_modified = true;
140146

141147
let members = workspace
142148
.members()
143149
.map(|p| LocalManifest::try_new(p.manifest_path()))
144150
.collect::<CargoResult<Vec<_>>>()?;
145151

146-
for dep in dependencies {
147-
if !dep_in_workspace(dep, &members) {
148-
remove_workspace_dep(dep, &mut manifest);
152+
let mut dependencies = members
153+
.iter()
154+
.flat_map(|manifest| {
155+
manifest.get_sections().into_iter().flat_map(|(_, table)| {
156+
table
157+
.as_table_like()
158+
.unwrap()
159+
.iter()
160+
.map(|(key, item)| Dependency::from_toml(&manifest.path, key, item))
161+
.collect::<Vec<_>>()
162+
})
163+
})
164+
.collect::<CargoResult<Vec<_>>>()?;
165+
166+
// Clean up the workspace.dependencies section and replace instances of
167+
// workspace dependencies with their definitions
168+
if let Some(toml_edit::Item::Table(deps_table)) = manifest
169+
.get_mut("workspace")
170+
.and_then(|t| t.get_mut("dependencies"))
171+
{
172+
deps_table.set_implicit(true);
173+
for (key, item) in deps_table.iter_mut() {
174+
let ws_dep = Dependency::from_toml(&workspace.root(), key.get(), item)?;
175+
176+
// search for uses of this workspace dependency
177+
let mut is_used = false;
178+
for dep in dependencies.iter_mut().filter(|d| {
179+
d.toml_key() == key.get() && matches!(d.source(), Some(Source::Workspace(_)))
180+
}) {
181+
// HACK: Replace workspace references in `dependencies` to simplify later GC steps:
182+
// 1. Avoid having to look it up again to determine the dependency source / spec
183+
// 2. The entry might get deleted, preventing us from looking it up again
184+
//
185+
// This does lose extra information, like features enabled, but that shouldn't be a
186+
// problem for GC
187+
*dep = ws_dep.clone();
188+
189+
is_used = true;
190+
}
191+
192+
if !is_used {
193+
*item = toml_edit::Item::None;
194+
is_modified = true;
195+
}
149196
}
150197
}
151198

152-
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
199+
// Clean up the profile section
200+
//
201+
// Example tables:
202+
// - profile.dev.package.foo
203+
// - profile.release.package."*"
204+
// - profile.release.package."foo:2.1.0"
205+
if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") {
206+
profile_section_table.set_implicit(true);
207+
208+
for (_, item) in profile_section_table.iter_mut() {
209+
if let toml_edit::Item::Table(profile_table) = item {
210+
profile_table.set_implicit(true);
211+
212+
if let Some(toml_edit::Item::Table(package_table)) =
213+
profile_table.get_mut("package")
214+
{
215+
package_table.set_implicit(true);
216+
217+
for (key, item) in package_table.iter_mut() {
218+
if !spec_has_match(
219+
&PackageIdSpec::parse(key.get())?,
220+
&dependencies,
221+
workspace.config(),
222+
)? {
223+
*item = toml_edit::Item::None;
224+
is_modified = true;
225+
}
226+
}
227+
}
228+
}
229+
}
230+
}
231+
232+
// Clean up the patch section
233+
if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") {
234+
patch_section_table.set_implicit(true);
235+
236+
// The key in each of the subtables is a source (either a registry or a URL)
237+
for (source, item) in patch_section_table.iter_mut() {
238+
if let toml_edit::Item::Table(patch_table) = item {
239+
patch_table.set_implicit(true);
240+
241+
for (key, item) in patch_table.iter_mut() {
242+
let package_name =
243+
Dependency::from_toml(&workspace.root_manifest(), key.get(), item)?.name;
244+
if !source_has_match(
245+
&package_name,
246+
source.get(),
247+
&dependencies,
248+
workspace.config(),
249+
)? {
250+
*item = toml_edit::Item::None;
251+
}
252+
}
253+
}
254+
}
255+
}
256+
257+
// Clean up the replace section
258+
if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
259+
table.set_implicit(true);
260+
261+
for (key, item) in table.iter_mut() {
262+
if !spec_has_match(
263+
&PackageIdSpec::parse(key.get())?,
264+
&dependencies,
265+
workspace.config(),
266+
)? {
267+
*item = toml_edit::Item::None;
268+
is_modified = true;
269+
}
270+
}
271+
}
272+
273+
if is_modified {
274+
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
275+
}
153276

154277
Ok(())
155278
}
156279

157-
/// Get whether or not a dependency is depended upon in a workspace.
158-
fn dep_in_workspace(dep: &str, members: &[LocalManifest]) -> bool {
159-
members.iter().any(|manifest| {
160-
manifest.get_sections().iter().any(|(_, table)| {
161-
table
162-
.as_table_like()
163-
.unwrap()
164-
.get(dep)
165-
.and_then(|t| t.get("workspace"))
166-
.and_then(|v| v.as_bool())
167-
.unwrap_or(false)
168-
})
169-
})
280+
/// Check whether or not a package ID spec matches any non-workspace dependencies.
281+
fn spec_has_match(
282+
spec: &PackageIdSpec,
283+
dependencies: &[Dependency],
284+
config: &Config,
285+
) -> CargoResult<bool> {
286+
for dep in dependencies {
287+
if spec.name().as_str() != &dep.name {
288+
continue;
289+
}
290+
291+
let version_matches = match (spec.version(), dep.version()) {
292+
(Some(v), Some(vq)) => semver::VersionReq::parse(vq)?.matches(v),
293+
(Some(_), None) => false,
294+
(None, None | Some(_)) => true,
295+
};
296+
if !version_matches {
297+
continue;
298+
}
299+
300+
match dep.source_id(config)? {
301+
MaybeWorkspace::Other(source_id) => {
302+
if spec.url().map(|u| u == source_id.url()).unwrap_or(true) {
303+
return Ok(true);
304+
}
305+
}
306+
MaybeWorkspace::Workspace(_) => {}
307+
}
308+
}
309+
310+
Ok(false)
170311
}
171312

172-
/// Remove a dependency from a workspace manifest.
173-
fn remove_workspace_dep(dep: &str, ws_manifest: &mut toml_edit::Document) {
174-
if let Some(toml_edit::Item::Table(table)) = ws_manifest
175-
.get_mut("workspace")
176-
.and_then(|t| t.get_mut("dependencies"))
177-
{
178-
table.set_implicit(true);
179-
table.remove(dep);
313+
/// Check whether or not a source (URL or registry name) matches any non-workspace dependencies.
314+
fn source_has_match(
315+
name: &str,
316+
source: &str,
317+
dependencies: &[Dependency],
318+
config: &Config,
319+
) -> CargoResult<bool> {
320+
for dep in dependencies {
321+
if &dep.name != name {
322+
continue;
323+
}
324+
325+
match dep.source_id(config)? {
326+
MaybeWorkspace::Other(source_id) => {
327+
if source_id.is_registry() {
328+
if source_id.display_registry_name() == source
329+
|| source_id.url().as_str() == source
330+
{
331+
return Ok(true);
332+
}
333+
} else if source_id.is_git() {
334+
if source_id.url().as_str() == source {
335+
return Ok(true);
336+
}
337+
}
338+
}
339+
MaybeWorkspace::Workspace(_) => {}
340+
}
180341
}
342+
343+
Ok(false)
181344
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use cargo_test_support::basic_manifest;
2+
use cargo_test_support::compare::assert_ui;
3+
use cargo_test_support::curr_dir;
4+
use cargo_test_support::git;
5+
use cargo_test_support::project;
6+
use cargo_test_support::CargoCommand;
7+
8+
use crate::cargo_remove::init_registry;
9+
10+
#[cargo_test]
11+
fn case() {
12+
init_registry();
13+
14+
let git_project1 = git::new("bar1", |project| {
15+
project
16+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
17+
.file("src/lib.rs", "")
18+
})
19+
.url();
20+
21+
let git_project2 = git::new("bar2", |project| {
22+
project
23+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
24+
.file("src/lib.rs", "")
25+
})
26+
.url();
27+
28+
let in_project = project()
29+
.file(
30+
"Cargo.toml",
31+
&format!(
32+
"[workspace]\n\
33+
members = [ \"my-member\" ]\n\
34+
\n\
35+
[package]\n\
36+
name = \"my-project\"\n\
37+
version = \"0.1.0\"\n\
38+
\n\
39+
[dependencies]\n\
40+
bar = {{ git = \"{git_project1}\" }}\n\
41+
\n\
42+
[patch.\"{git_project1}\"]\n\
43+
bar = {{ git = \"{git_project2}\" }}\n\
44+
\n\
45+
[patch.crates-io]\n\
46+
bar = {{ git = \"{git_project2}\" }}\n",
47+
),
48+
)
49+
.file("src/lib.rs", "")
50+
.file(
51+
"my-member/Cargo.toml",
52+
"[package]\n\
53+
name = \"my-member\"\n\
54+
version = \"0.1.0\"\n\
55+
\n\
56+
[dependencies]\n\
57+
bar = \"0.1.0\"\n",
58+
)
59+
.file("my-member/src/lib.rs", "")
60+
.build();
61+
62+
snapbox::cmd::Command::cargo_ui()
63+
.arg("remove")
64+
.args(["bar"])
65+
.current_dir(&in_project.root())
66+
.assert()
67+
.success()
68+
.stdout_matches_path(curr_dir!().join("stdout.log"))
69+
.stderr_matches_path(curr_dir!().join("stderr.log"));
70+
71+
assert_ui().subset_matches(curr_dir!().join("out"), &in_project.root());
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[workspace]
2+
members = [ "my-member" ]
3+
4+
[package]
5+
name = "my-project"
6+
version = "0.1.0"
7+
8+
[patch.crates-io]
9+
bar = { git = "[ROOTURL]/bar2" }

tests/testsuite/cargo_remove/gc_patch/out/src/lib.rs

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Removing bar from dependencies
2+
Updating git repository `[ROOTURL]/bar2`
3+
Updating `dummy-registry` index

tests/testsuite/cargo_remove/gc_patch/stdout.log

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
clippy = "0.4"
18+
19+
[dev-dependencies]
20+
regex = "0.1.1"
21+
serde = "1.0.90"
22+
toml = "0.2.3"
23+
docopt = "0.6"
24+
25+
[features]
26+
std = ["serde/std", "semver/std"]
27+
28+
[profile.dev.package.docopt]
29+
opt-level = 3
30+
31+
[profile.dev.package."toml@0.1.0"]
32+
opt-level = 3
33+
34+
[profile.release.package.toml]
35+
opt-level = 1
36+
overflow-checks = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)