Skip to content

Commit d95c2a2

Browse files
committed
Add a global cache garbage collector.
This adds a garbage collector which will remove old files from cargo's global cache. A general overview of the changes here: - `cargo::core::global_cache_tracker` contains the `GlobalCacheTracker` which handles the interface to a sqlite database which stores timestamps of the last time a file was used. - `DeferredGlobalLastUse` is a type that implements an optimization for collecting last-use timestamps so that they can be flushed to disk all at once. - `cargo::core::gc` contains the `Gc` type which is the interface for performing garbage collection. It coordinates with the `GlobalCacheTracker` for determining what to delete. - Garbage collection can either be automatic or manual. The automatic garbage collection supports some config options for defining when it runs and how much it deletes. - Manual garbage collection can be performed via options to `cargo clean`. - `cargo clean` uses the new package cache locking system to coordinate access to the package cache to prevent interference with other cargo commands running concurrently.
1 parent a788bea commit d95c2a2

File tree

22 files changed

+4645
-52
lines changed

22 files changed

+4645
-52
lines changed

Cargo.lock

Lines changed: 20 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pretty_assertions = "1.4.0"
7171
proptest = "1.2.0"
7272
pulldown-cmark = { version = "0.9.3", default-features = false }
7373
rand = "0.8.5"
74+
regex = "1.9.3"
7475
rusqlite = { version = "0.29.0", features = ["bundled"] }
7576
rustfix = "0.6.1"
7677
same-file = "1.0.6"
@@ -161,6 +162,7 @@ pasetors.workspace = true
161162
pathdiff.workspace = true
162163
pulldown-cmark.workspace = true
163164
rand.workspace = true
165+
regex.workspace = true
164166
rusqlite.workspace = true
165167
rustfix.workspace = true
166168
semver.workspace = true

src/bin/cargo/commands/clean.rs

Lines changed: 224 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use crate::command_prelude::*;
2-
2+
use cargo::core::gc::{parse_human_size, parse_time_span};
3+
use cargo::core::gc::{AutoGcKind, GcOpts};
34
use cargo::ops::{self, CleanOptions};
45
use cargo::util::print_available_packages;
6+
use cargo::CargoResult;
7+
use clap::builder::{PossibleValuesParser, TypedValueParser};
8+
use std::time::Duration;
59

610
pub fn cli() -> Command {
711
subcommand("clean")
@@ -17,29 +21,243 @@ pub fn cli() -> Command {
1721
.arg(
1822
flag(
1923
"dry-run",
20-
"Display what would be deleted without deleting anything",
24+
"Display what would be deleted without deleting anything (unstable)",
2125
)
2226
.short('n'),
2327
)
28+
29+
// NOTE: Not all of these options may get stabilized. Some of them are
30+
// very low-level details, and may not be something typical users need.
31+
.arg(
32+
optional_opt(
33+
"gc",
34+
"Delete old and unused files (unstable) (comma separated): all, download, target, shared-target",
35+
)
36+
.hide(true)
37+
.value_name("KINDS")
38+
.value_parser(
39+
PossibleValuesParser::new(["all", "download", "target", "shared-target"]).map(|x|
40+
match x.as_str() {
41+
"all" => AutoGcKind::All,
42+
"download" => AutoGcKind::Download,
43+
"target" => panic!("target is not yet implemented"),
44+
"shared-target" => panic!("shared-target is not yet implemented"),
45+
x => panic!("possible value out of sync with `{x}`"),
46+
}
47+
))
48+
.require_equals(true),
49+
)
50+
.arg(
51+
opt(
52+
"max-src-age",
53+
"Deletes source cache files that have not been used since the given age (unstable)",
54+
)
55+
.hide(true)
56+
.value_name("DURATION"),
57+
)
58+
.arg(
59+
opt(
60+
"max-crate-age",
61+
"Deletes crate cache files that have not been used since the given age (unstable)",
62+
)
63+
.hide(true)
64+
.value_name("DURATION"),
65+
)
66+
.arg(
67+
opt(
68+
"max-index-age",
69+
"Deletes registry indexes that have not been used since then given age (unstable)",
70+
)
71+
.hide(true)
72+
.value_name("DURATION"),
73+
)
74+
.arg(
75+
opt(
76+
"max-git-co-age",
77+
"Deletes git dependency checkouts that have not been used since then given age (unstable)",
78+
)
79+
.hide(true)
80+
.value_name("DURATION"),
81+
)
82+
.arg(
83+
opt(
84+
"max-git-db-age",
85+
"Deletes git dependency clones that have not been used since then given age (unstable)",
86+
)
87+
.hide(true)
88+
.value_name("DURATION"),
89+
)
90+
.arg(
91+
opt(
92+
"max-download-age",
93+
"Deletes any downloaded cache data that has not been used since then given age (unstable)",
94+
)
95+
.hide(true)
96+
.value_name("DURATION"),
97+
)
98+
99+
.arg(
100+
opt(
101+
"max-src-size",
102+
"Deletes source cache files until the cache is under the given size (unstable)",
103+
)
104+
.hide(true)
105+
.value_name("SIZE"),
106+
)
107+
.arg(
108+
opt(
109+
"max-crate-size",
110+
"Deletes crate cache files until the cache is under the given size (unstable)",
111+
)
112+
.hide(true)
113+
.value_name("SIZE"),
114+
)
115+
.arg(
116+
opt("max-git-size",
117+
"Deletes git dependency caches until the cache is under the given size (unstable")
118+
.hide(true)
119+
.value_name("SIZE"))
120+
.arg(
121+
opt(
122+
"max-download-size",
123+
"Deletes downloaded cache data until the cache is under the given size (unstable)",
124+
)
125+
.hide(true)
126+
.value_name("DURATION"),
127+
)
128+
129+
// These are unimplemented. Leaving here as a guide for how this is
130+
// intended to evolve. These will likely change, this is just a sketch
131+
// of ideas.
132+
.arg(
133+
opt(
134+
"max-target-age",
135+
"Deletes any build artifact files that have not been used since then given age (unstable) (UNIMPLEMENTED)",
136+
)
137+
.hide(true)
138+
.value_name("DURATION"),
139+
)
140+
.arg(
141+
// TODO: come up with something less wordy?
142+
opt(
143+
"max-shared-target-age",
144+
"Deletes any shared build artifact files that have not been used since then given age (unstable) (UNIMPLEMENTED)",
145+
)
146+
.hide(true)
147+
.value_name("DURATION"),
148+
)
149+
.arg(
150+
opt(
151+
"max-target-size",
152+
"Deletes build artifact files until the cache is under the given size (unstable) (UNIMPLEMENTED)",
153+
)
154+
.hide(true)
155+
.value_name("SIZE"),
156+
)
157+
.arg(
158+
// TODO: come up with something less wordy?
159+
opt(
160+
"max-shared-target-size",
161+
"Deletes shared build artifact files until the cache is under the given size (unstable) (UNIMPLEMENTED)",
162+
)
163+
.hide(true)
164+
.value_name("DURATION"),
165+
)
166+
24167
.after_help("Run `cargo help clean` for more detailed information.\n")
25168
}
26169

27170
pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult {
28-
let ws = args.workspace(config)?;
171+
let ws = args.workspace(config);
29172

30173
if args.is_present_with_zero_values("package") {
31-
print_available_packages(&ws)?;
174+
print_available_packages(&ws?)?;
175+
return Ok(());
32176
}
33177

178+
let unstable_gc = |opt| {
179+
// TODO: issue number
180+
config
181+
.cli_unstable()
182+
.fail_if_stable_opt_custom_z(opt, 0, "gc", config.cli_unstable().gc)
183+
};
184+
let unstable_cache_opt = |opt| -> CargoResult<Option<&str>> {
185+
let arg = args.get_one::<String>(opt).map(String::as_str);
186+
if arg.is_some() {
187+
unstable_gc(opt)?;
188+
}
189+
Ok(arg)
190+
};
191+
let unstable_size_opt = |opt| -> CargoResult<Option<u64>> {
192+
unstable_cache_opt(opt)?
193+
.map(|s| parse_human_size(s))
194+
.transpose()
195+
};
196+
let unstable_duration_opt = |opt| -> CargoResult<Option<Duration>> {
197+
unstable_cache_opt(opt)?
198+
.map(|s| parse_time_span(s))
199+
.transpose()
200+
};
201+
let unimplemented_opt = |opt| -> CargoResult<Option<&str>> {
202+
let arg = args.get_one::<String>(opt).map(String::as_str);
203+
if arg.is_some() {
204+
anyhow::bail!("option --{opt} is not yet implemented");
205+
}
206+
Ok(None)
207+
};
208+
let unimplemented_size_opt = |opt| -> CargoResult<Option<u64>> {
209+
unimplemented_opt(opt)?;
210+
Ok(None)
211+
};
212+
let unimplemented_duration_opt = |opt| -> CargoResult<Option<Duration>> {
213+
unimplemented_opt(opt)?;
214+
Ok(None)
215+
};
216+
let dry_run = args.flag("dry-run");
217+
if dry_run {
218+
unstable_gc("dry-run")?;
219+
}
220+
221+
let mut gc: Vec<_> = args
222+
.get_many::<AutoGcKind>("gc")
223+
.unwrap_or_default()
224+
.cloned()
225+
.collect();
226+
if gc.is_empty() && args.contains_id("gc") {
227+
gc.push(AutoGcKind::All);
228+
}
229+
if !gc.is_empty() {
230+
unstable_gc("gc")?;
231+
}
232+
233+
let mut gc_opts = GcOpts {
234+
max_src_age: unstable_duration_opt("max-src-age")?,
235+
max_crate_age: unstable_duration_opt("max-crate-age")?,
236+
max_index_age: unstable_duration_opt("max-index-age")?,
237+
max_git_co_age: unstable_duration_opt("max-git-co-age")?,
238+
max_git_db_age: unstable_duration_opt("max-git-db-age")?,
239+
max_src_size: unstable_size_opt("max-src-size")?,
240+
max_crate_size: unstable_size_opt("max-crate-size")?,
241+
max_git_size: unstable_size_opt("max-git-size")?,
242+
max_download_size: unstable_size_opt("max-download-size")?,
243+
max_target_age: unimplemented_duration_opt("max-target-age")?,
244+
max_shared_target_age: unimplemented_duration_opt("max-shared-target-age")?,
245+
max_target_size: unimplemented_size_opt("max-target-size")?,
246+
max_shared_target_size: unimplemented_size_opt("max-shared-target-size")?,
247+
};
248+
let max_download_age = unstable_duration_opt("max-download-age")?;
249+
gc_opts.update_for_auto_gc(config, &gc, max_download_age)?;
250+
34251
let opts = CleanOptions {
35252
config,
36253
spec: values(args, "package"),
37254
targets: args.targets()?,
38255
requested_profile: args.get_profile_name(config, "dev", ProfileChecking::Custom)?,
39256
profile_specified: args.contains_id("profile") || args.flag("release"),
40257
doc: args.flag("doc"),
41-
dry_run: args.flag("dry-run"),
258+
dry_run,
259+
gc_opts,
42260
};
43-
ops::clean(&ws, &opts)?;
261+
ops::clean(ws, &opts)?;
44262
Ok(())
45263
}

0 commit comments

Comments
 (0)