disk-space-saviour 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { scanItems } from '../env/scope.js';
1
2
  /**
2
3
  * Global per-user caches (ecosystem download and compilation caches, IDE and
3
4
  * browser caches), system package caches, and leftovers in temporary
@@ -9,13 +10,16 @@ import { OTHER_RULES } from '../rules/other.js';
9
10
  import { APP_RULES, expandChromiumCaches } from '../rules/apps.js';
10
11
  import { compareVersions } from '../rules/versions.js';
11
12
  import { block, makeItem } from '../items.js';
12
- import { expandGlobPaths, expandRulePath } from '../paths.js';
13
- import { olderThan, scanTimeBusy } from './common.js';
13
+ import { expandGlobPaths, expandRulePath, isWithin } from '../paths.js';
14
+ import { olderThan, scanTimeBusy, resolvePaths } from './common.js';
15
+ import { configuredPattern } from '../env/cache-roots.js';
16
+ import { TOOLCHAIN_CACHE_RULES } from '../rules/toolchains.js';
14
17
 
15
18
  export const GLOBAL_RULES = [
16
19
  ...CACHE_RULES,
17
20
  ...expandChromiumCaches(OTHER_RULES),
18
21
  ...APP_RULES,
22
+ ...TOOLCHAIN_CACHE_RULES,
19
23
  ];
20
24
 
21
25
  function scopeBases(rule, homes, tmpDirs) {
@@ -32,15 +36,30 @@ function scopeBases(rule, homes, tmpDirs) {
32
36
  function rulePatterns(env, rule, base) {
33
37
  const patterns = [];
34
38
  for (const pattern of rule.paths) {
39
+ if (pattern.includes('{GEM_PATH}')) {
40
+ if (base.home === env.currentHome) {
41
+ for (const root of (env.vars.GEM_PATH ?? '').split(
42
+ env.platform === 'win32' ? ';' : ':'
43
+ )) {
44
+ if (env.path.isAbsolute(root)) {
45
+ patterns.push(pattern.replace('{GEM_PATH}', root));
46
+ }
47
+ }
48
+ }
49
+ continue;
50
+ }
35
51
  const usesVars = /\{(?!TMP\})[A-Z_]+\}/.test(pattern);
36
52
  if (usesVars && base.home !== env.currentHome) {
37
53
  continue;
38
54
  }
39
- const expanded = expandRulePath(pattern, {
40
- home: base.home ?? '/',
41
- vars: { ...env.vars, TMP: base.tmp ?? undefined },
42
- pathApi: env.path,
43
- });
55
+ const expanded = expandRulePath(
56
+ configuredPattern(env, base.home, pattern),
57
+ {
58
+ home: base.home ?? '/',
59
+ vars: { ...env.vars, TMP: base.tmp ?? undefined },
60
+ pathApi: env.path,
61
+ }
62
+ );
44
63
  if (expanded) {
45
64
  patterns.push(expanded);
46
65
  }
@@ -59,15 +78,26 @@ async function nativeAction(context, rule, base, paths) {
59
78
  if (!native || options.noNative) {
60
79
  return { action: removal, blocker: null };
61
80
  }
62
- const usable =
63
- (rule.scope === 'system' || base.home === env.currentHome) &&
64
- (await env.which(native.tool)) &&
65
- (!native.root || (await env.isRoot()));
81
+ const init = native.init
82
+ ? expandRulePath(configuredPattern(env, base.home, native.init), {
83
+ home: base.home,
84
+ pathApi: env.path,
85
+ })
86
+ : null;
87
+ const nativeRoot = native.cacheRoot
88
+ ? expandRulePath(configuredPattern(env, base.home, native.cacheRoot), {
89
+ home: base.home,
90
+ pathApi: env.path,
91
+ })
92
+ : null;
93
+ const usable = await nativeUsable(env, rule, base, paths, nativeRoot, init);
66
94
  if (usable) {
67
95
  return {
68
96
  action: {
69
97
  type: 'command',
70
- argv: native.argv,
98
+ argv: init
99
+ ? [native.tool, '-c', `. "$1" && ${native.command}`, 'dss', init]
100
+ : native.argv,
71
101
  measure: paths,
72
102
  },
73
103
  blocker: null,
@@ -82,6 +112,16 @@ async function nativeAction(context, rule, base, paths) {
82
112
  return { action: removal, blocker: null };
83
113
  }
84
114
 
115
+ async function nativeUsable(env, rule, base, paths, root, init) {
116
+ return (
117
+ (rule.scope === 'system' || base.home === env.currentHome) &&
118
+ (await env.which(rule.native.tool)) &&
119
+ (!rule.native.root || (await env.isRoot())) &&
120
+ (!root || paths.every((target) => isWithin(target, root, env.path))) &&
121
+ (!init || (await env.exists(init)))
122
+ );
123
+ }
124
+
85
125
  function ageFilter(context, rule) {
86
126
  const { options } = context;
87
127
  if (rule.minAge === 'stale') {
@@ -96,13 +136,13 @@ function ageFilter(context, rule) {
96
136
  }
97
137
 
98
138
  /** Keep the latest installed revision of each browser and platform family. */
99
- async function olderVersions(env, rule, paths) {
139
+ function olderVersions(env, rule, paths, stats) {
100
140
  if (!rule.versionPattern) {
101
141
  return paths;
102
142
  }
103
143
  const groups = new Map();
104
144
  for (const target of paths) {
105
- if ((await env.stat(target))?.type !== 'dir') {
145
+ if (stats.get(target)?.type !== 'dir') {
106
146
  continue;
107
147
  }
108
148
  const match = rule.versionPattern.exec(env.path.basename(target));
@@ -201,6 +241,14 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
201
241
  env,
202
242
  planned.flatMap((entry) => entry.patterns)
203
243
  );
244
+ const typedPaths = planned
245
+ .filter(
246
+ ({ rule }) => rule.directoryOnly || rule.fileOnly || rule.versionPattern
247
+ )
248
+ .flatMap(({ patterns }) =>
249
+ patterns.flatMap((pattern) => matches.get(pattern) ?? [])
250
+ );
251
+ const stats = await env.statMany([...new Set(typedPaths)]);
204
252
  const found = [];
205
253
  const seen = new Set();
206
254
  for (const { rule, base, patterns } of planned) {
@@ -210,19 +258,23 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
210
258
 
211
259
  const accepted = [];
212
260
  for (const target of candidates) {
213
- if (!(await acceptedPath(env, rule, target))) {
261
+ if (!(await acceptedPath(env, rule, target, stats))) {
214
262
  continue;
215
263
  }
216
264
  accepted.push(target);
217
265
  seen.add(target);
218
266
  }
219
- const paths = await olderVersions(env, rule, accepted);
267
+ const paths = olderVersions(env, rule, accepted, stats);
220
268
  if (paths.length > 0) {
221
269
  found.push({ rule, base, paths });
222
270
  }
223
271
  }
224
272
  const usages = await env.usageMany(found.flatMap((entry) => entry.paths));
225
- const items = [];
273
+ await resolvePaths(
274
+ context,
275
+ found.flatMap((entry) => entry.paths)
276
+ );
277
+ const items = scanItems(context);
226
278
  for (const { rule, base, paths } of found) {
227
279
  const accept = ageFilter(context, rule);
228
280
  const measured = paths
@@ -238,15 +290,15 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
238
290
  return items;
239
291
  }
240
292
 
241
- async function acceptedPath(env, rule, target) {
293
+ async function acceptedPath(env, rule, target, stats) {
242
294
  const name = env.path.basename(target);
243
295
  if (rule.excludeNames?.includes(name)) {
244
296
  return false;
245
297
  }
246
- if (rule.directoryOnly && (await env.stat(target))?.type !== 'dir') {
298
+ if (rule.directoryOnly && stats.get(target)?.type !== 'dir') {
247
299
  return false;
248
300
  }
249
- if (rule.fileOnly && (await env.stat(target))?.type !== 'file') {
301
+ if (rule.fileOnly && stats.get(target)?.type !== 'file') {
250
302
  return false;
251
303
  }
252
304
  if (
@@ -1,3 +1,4 @@
1
+ import { scanItems } from '../env/scope.js';
1
2
  /**
2
3
  * Per-project dependency, build and cache directories (node_modules, target,
3
4
  * .venv, ...) found by walking the scan roots.
@@ -261,7 +262,31 @@ const RUST_PARTS = {
261
262
  */
262
263
  async function rustItems(context, targetItem, rule) {
263
264
  const { env, liveness, options, now } = context;
264
- const items = [];
265
+ const items = scanItems(context);
266
+ const semverPath = env.path.join(targetItem.path, 'semver-checks');
267
+ const semverUsage = await env.usage(semverPath);
268
+ if (semverUsage) {
269
+ const semver = makeItem(env, {
270
+ rule: 'cargo-semver-checks-cache',
271
+ kind: 'cache',
272
+ ecosystem: 'rust',
273
+ description: 'cargo-semver-checks baseline builds',
274
+ path: semverPath,
275
+ bytes: semverUsage.bytes,
276
+ newestMtimeMs: semverUsage.newestMtimeMs,
277
+ project: targetItem.project,
278
+ parentId: targetItem.id,
279
+ tier: 'safe',
280
+ reason: 'baseline rustdoc and builds are regenerated on demand',
281
+ checks: {
282
+ busy: ['cargo-semver-checks', ...rule.busy],
283
+ cwd: targetItem.project,
284
+ mtime: true,
285
+ },
286
+ });
287
+ await addBlockers(context, semver, false);
288
+ items.push(semver);
289
+ }
265
290
  for (const profile of await findRustProfiles(env, targetItem.path)) {
266
291
  const result = await analyzeRustProfile(env, profile, {
267
292
  staleAgeMs: options.staleAgeMs,
@@ -315,7 +340,7 @@ export async function scanProjects(context, { rules = PROJECT_RULES } = {}) {
315
340
  });
316
341
  const usages = await env.usageMany(matches.map((m) => m.candidate.path));
317
342
  const globs = [...new Set(rules.flatMap((rule) => rule.names))];
318
- const items = [];
343
+ const items = scanItems(context);
319
344
  for (const match of matches) {
320
345
  const usage = usages.get(match.candidate.path);
321
346
  if (!usage) {
@@ -37,7 +37,13 @@ const HASHED = /^(.+)-([0-9a-f]{16})(\..*)?$/;
37
37
  const INCREMENTAL = /^(.+)-([0-9a-z]{8,20})$/;
38
38
  const LIB_PREFIXED = new Set(['.rlib', '.rmeta', '.so', '.dylib', '.a']);
39
39
  const KIND_ORDER = ['lib', 'dylib', 'staticlib', 'bin', 'rmeta', 'other'];
40
- const NON_PROFILES = new Set(['doc', 'package', 'tmp', 'flycheck0']);
40
+ const NON_PROFILES = new Set([
41
+ 'doc',
42
+ 'package',
43
+ 'tmp',
44
+ 'flycheck0',
45
+ 'semver-checks',
46
+ ]);
41
47
  const EXECUTABLE = new Set(['', '.exe']);
42
48
  export const DEFAULT_GENERATION_MS = 10 * 60 * 1000;
43
49
  export const DEFAULT_LEAF_IDLE_MS = 3 * 60 * 60 * 1000;
@@ -1,3 +1,4 @@
1
+ import { scanItems } from '../env/scope.js';
1
2
  /**
2
3
  * System reclaimers that only the owning tool can clean correctly:
3
4
  * journald archives (`journalctl --vacuum-size`), disabled snap revisions
@@ -102,7 +103,7 @@ async function snapItems(context, isRoot) {
102
103
  return [];
103
104
  }
104
105
  const result = await env.run(['snap', 'list', '--all']);
105
- const items = [];
106
+ const items = scanItems(context);
106
107
  for (const { name, revision } of parseDisabledSnaps(result.stdout)) {
107
108
  const file = `/var/lib/snapd/snaps/${name}_${revision}.snap`;
108
109
  const stat = await env.stat(file);
@@ -163,9 +164,9 @@ async function brewItems(context, isRoot) {
163
164
  */
164
165
  export async function scanSystem(context) {
165
166
  const isRoot = await context.env.isRoot();
166
- return [
167
- ...(await journalItems(context, isRoot)),
168
- ...(await snapItems(context, isRoot)),
169
- ...(await brewItems(context, isRoot)),
170
- ];
167
+ const items = scanItems(context);
168
+ for (const scanner of [journalItems, snapItems, brewItems]) {
169
+ items.push(...(await scanner(context, isRoot)));
170
+ }
171
+ return items;
171
172
  }
@@ -1,3 +1,4 @@
1
+ import { scanItems } from '../env/scope.js';
1
2
  /**
2
3
  * Toolchain versions installed by version managers (nvm, pyenv, rbenv,
3
4
  * SDKMAN!, rustup, elan, ghcup, opam, swiftly, VS Code Server).
@@ -15,21 +16,25 @@ import {
15
16
  } from '../rules/versions.js';
16
17
  import { block, makeItem } from '../items.js';
17
18
  import { expandGlobPath, expandRulePath } from '../paths.js';
18
- import { scanTimeBusy } from './common.js';
19
+ import { scanTimeBusy, resolvePaths } from './common.js';
20
+ import { configuredPattern } from '../env/cache-roots.js';
19
21
 
20
22
  async function installDirs(env, rule, home) {
21
23
  const dirs = [];
22
24
  for (const pattern of rule.dirs) {
23
- const expanded = expandRulePath(pattern, { home, pathApi: env.path });
24
- for (const candidate of await expandGlobPath(env, expanded)) {
25
+ const expanded = expandRulePath(configuredPattern(env, home, pattern), {
26
+ home,
27
+ pathApi: env.path,
28
+ });
29
+ const candidates = await expandGlobPath(env, expanded);
30
+ const stats = await env.statMany(candidates);
31
+ for (const candidate of candidates) {
25
32
  const name = env.path.basename(candidate);
26
33
  if ((rule.exclude ?? []).includes(name)) {
27
34
  continue;
28
35
  }
29
36
  if (
30
- !(rule.installTypes ?? ['dir']).includes(
31
- (await env.stat(candidate))?.type
32
- )
37
+ !(rule.installTypes ?? ['dir']).includes(stats.get(candidate)?.type)
33
38
  ) {
34
39
  continue;
35
40
  }
@@ -80,7 +85,10 @@ function linkRefs(target) {
80
85
  */
81
86
  async function managerRefs(env, rule, home, installs) {
82
87
  const expand = (pattern) =>
83
- expandRulePath(pattern, { home, pathApi: env.path });
88
+ expandRulePath(configuredPattern(env, home, pattern), {
89
+ home,
90
+ pathApi: env.path,
91
+ });
84
92
  const refs = [];
85
93
  for (const source of rule.defaults ?? []) {
86
94
  const text = await env.readText(expand(source.path), 65536);
@@ -183,9 +191,10 @@ async function ruleItems(context, rule, home, pins) {
183
191
  }
184
192
  const refs = [...(await managerRefs(env, rule, home, installs)), ...pins];
185
193
  const usages = await env.usageMany(installs);
194
+ await resolvePaths(context, installs);
186
195
  const newest = newestInstalls(env, rule, installs, usages);
187
196
  const versionOf = rule.versionOf ?? ((dir) => env.path.basename(dir));
188
- const items = [];
197
+ const items = scanItems(context);
189
198
  for (const dir of installs) {
190
199
  const version = versionOf(dir);
191
200
  if (newest.has(dir) || refs.some((ref) => versionMatches(version, ref))) {
@@ -232,7 +241,7 @@ export async function scanVersions(context, { rules = VERSION_RULES } = {}) {
232
241
  rules,
233
242
  options.maxDepth ?? 6
234
243
  );
235
- const items = [];
244
+ const items = scanItems(context);
236
245
  for (const rule of rules) {
237
246
  for (const home of homes) {
238
247
  items.push(...(await ruleItems(context, rule, home, pins.get(rule.id))));