Skip to content

Commit 818f10b

Browse files
authored
Mark package-configs as active when writing (#4592)
1 parent d59fb52 commit 818f10b

File tree

8 files changed

+220
-7
lines changed

8 files changed

+220
-7
lines changed

doc/cache_layout.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Here are the top-level folders you can find in a Pub cache.
3434
```plaintext
3535
$PUB_CACHE/
3636
├── global_packages/ # Globally activated packages
37+
├── active_roots/ # Information about packages that this cache caches for.
3738
├── bin/ # Executables compiled from globally activated packages.
3839
├── git/ # Cloned git packages
3940
├── hosted/ # Hosted package downloads
@@ -233,9 +234,30 @@ $PUB_CACHE/bin/
233234
└── stagehand
234235
```
235236

237+
# Active roots
238+
$PUB_CACHE/active_roots/
239+
240+
In order to be able to prune the cache (`dart pub cache gc`) pub keeps a tally of
241+
each time it writes a `.dart_tool/package_config.json` file (an activation).
242+
243+
The directory is laid out such that each file-name is the hex-encoded sha256
244+
hash of the absolute file-uri of the path of the package config.
245+
246+
The first two bytes are used for a subdirectory, to prevent too many files in
247+
one directory.
248+
249+
When implemented `dart pub cache gc` will look through all the package configs,
250+
and mark all cached packages in the cache used by those projects `alive`. If a
251+
package config doesn't exist, it is ignored, and the file marking it is deleted.
252+
253+
All other packages in the cache are removed.
254+
255+
Packages that are installed in the cache within 1 day are not deleted. This is
256+
to minimize the risk of race-conditions.
257+
236258
## Logs
237259

238260
When pub crashes or is run with `--verbose` it will create a
239261
`$PUB_CACHE/log/pub_log.txt` with the dart sdk version, platform, `$PUB_CACHE`,
240-
`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log and
241-
stack-trace.
262+
`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log
263+
and stack-trace.

lib/src/entrypoint.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@ See $workspacesDocUrl for more information.''',
402402
/// If the workspace is non-trivial: For each package in the workspace write:
403403
/// `.dart_tool/pub/workspace_ref.json` with a pointer to the workspace root
404404
/// package dir.
405+
///
406+
/// Also marks the package active in `PUB_CACHE/active_roots/`.
405407
Future<void> writePackageConfigFiles() async {
406408
ensureDir(p.dirname(packageConfigPath));
407409

@@ -433,6 +435,9 @@ See $workspacesDocUrl for more information.''',
433435
writeTextFileIfDifferent(workspaceRefPath, '$workspaceRef\n');
434436
}
435437
}
438+
if (lockFile.packages.values.any((id) => id.source is CachedSource)) {
439+
cache.markRootActive(packageConfigPath);
440+
}
436441
}
437442

438443
Future<String> _packageGraphFile(SystemCache cache) async {

lib/src/global_packages.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,14 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
293293

294294
lockFile.writeToFile(p.join(tempDir, 'pubspec.lock'), cache);
295295

296+
final packageDir = _packageDir(name);
297+
tryDeleteEntry(packageDir);
298+
tryRenameDir(tempDir, packageDir);
299+
296300
// Load the package graph from [result] so we don't need to re-parse all
297301
// the pubspecs.
298302
final entrypoint = Entrypoint.global(
299-
root,
303+
packageForConstraint(dep, packageDir),
300304
lockFile,
301305
cache,
302306
solveResult: result,
@@ -305,9 +309,6 @@ To recompile executables, first run `$topLevelProgram pub global deactivate $nam
305309
await entrypoint.writePackageConfigFiles();
306310

307311
await entrypoint.precompileExecutables();
308-
309-
tryDeleteEntry(_packageDir(name));
310-
tryRenameDir(tempDir, _packageDir(name));
311312
}
312313

313314
final entrypoint = Entrypoint.global(

lib/src/io.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ void writeTextFile(
257257
File(file).writeAsStringSync(contents, encoding: encoding);
258258
}
259259

260+
/// Reads the file at [path] and writes [newContent] to it, if it is different
261+
/// from [newContent].
262+
///
263+
/// If the file doesn't exist it is always written.
260264
void writeTextFileIfDifferent(String path, String newContent) {
261265
// Compare to the present package_config.json
262266
// For purposes of equality we don't care about the `generated` timestamp.

lib/src/system_cache.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:convert';
56
import 'dart:io';
67

8+
import 'package:crypto/crypto.dart';
79
import 'package:path/path.dart' as p;
810
import 'package:pub_semver/pub_semver.dart';
911

@@ -80,6 +82,8 @@ Consider setting the `PUB_CACHE` variable manually.
8082

8183
Source get defaultSource => hosted;
8284

85+
late final Iterable<CachedSource> cachedSources = [hosted, git];
86+
8387
/// The built-in Git source.
8488
GitSource get git => GitSource.instance;
8589

@@ -400,6 +404,74 @@ https://dart.dev/go/pub-cache
400404
}
401405

402406
bool _hasMaintainedCache = false;
407+
408+
late final _activeRootsDir = p.join(rootDir, 'active_roots');
409+
410+
/// Returns the paths of all packages_configs registered in
411+
/// [_activeRootsDir].
412+
List<String> activeRoots() {
413+
final List<String> files;
414+
try {
415+
files = listDir(_activeRootsDir, includeDirs: false, recursive: true);
416+
} on IOException {
417+
return [];
418+
}
419+
final activeRoots = <String>[];
420+
for (final file in files) {
421+
final Object? decoded;
422+
try {
423+
decoded = jsonDecode(readTextFile(file));
424+
} on IOException catch (e) {
425+
log.fine('Could not read $file $e - deleting');
426+
tryDeleteEntry(file);
427+
continue;
428+
} on FormatException catch (e) {
429+
log.fine('Could not decode $file $e - deleting');
430+
tryDeleteEntry(file);
431+
continue;
432+
}
433+
if (decoded is! Map<String, Object?>) {
434+
log.fine('Faulty $file - deleting');
435+
tryDeleteEntry(file);
436+
continue;
437+
}
438+
final uriText = decoded['package_config'];
439+
if (uriText is! String) {
440+
log.fine('Faulty $file - deleting');
441+
tryDeleteEntry(file);
442+
continue;
443+
}
444+
final uri = Uri.tryParse(uriText);
445+
if (uri == null || !uri.isScheme('file')) {
446+
log.fine('Faulty $file - deleting');
447+
tryDeleteEntry(file);
448+
continue;
449+
}
450+
activeRoots.add(uri.toFilePath());
451+
}
452+
return activeRoots;
453+
}
454+
455+
/// Adds a file to the `PUB_CACHE/active_roots/` dir indicating
456+
/// [packageConfigPath] is active.
457+
void markRootActive(String packageConfigPath) {
458+
final canonicalFileUri =
459+
p.toUri(p.canonicalize(packageConfigPath)).toString();
460+
461+
final hash = hexEncode(sha256.convert(utf8.encode(canonicalFileUri)).bytes);
462+
463+
final firstTwo = hash.substring(0, 2);
464+
final theRest = hash.substring(2);
465+
466+
final dir = p.join(_activeRootsDir, firstTwo);
467+
ensureDir(dir);
468+
469+
final filename = p.join(dir, theRest);
470+
writeTextFileIfDifferent(
471+
filename,
472+
'${jsonEncode({'package_config': canonicalFileUri})}\n',
473+
);
474+
}
403475
}
404476

405477
typedef SourceRegistry = Source Function(String? name);

test/cache/gc_test.dart

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:crypto/crypto.dart';
5+
import 'package:path/path.dart' as p;
6+
import 'package:pub/src/system_cache.dart';
7+
import 'package:pub/src/utils.dart';
8+
import 'package:test/test.dart';
9+
10+
import '../descriptor.dart' as d;
11+
import '../test_pub.dart';
12+
13+
void main() async {
14+
test('marks a package active on pub get and global activate', () async {
15+
final server = await servePackages();
16+
17+
server.serve('foo', '1.0.0');
18+
server.serve('bar', '1.0.0');
19+
20+
await runPub(args: ['global', 'activate', 'foo']);
21+
22+
// Without cached dependencies we don't register the package
23+
await d.dir('app_none', [d.appPubspec()]).create();
24+
25+
await d.appDir(dependencies: {'bar': '1.0.0'}).create();
26+
27+
await d.dir('app_hosted', [
28+
d.appPubspec(dependencies: {'bar': '^1.0.0'}),
29+
]).create();
30+
31+
await d.git('lib', [d.libPubspec('lib', '1.0.0')]).create();
32+
33+
await d.dir('app_git', [
34+
d.appPubspec(
35+
dependencies: {
36+
'lib': {'git': '../lib'},
37+
},
38+
),
39+
]).create();
40+
41+
await d.dir('app_path', [
42+
d.appPubspec(
43+
dependencies: {
44+
'lib': {'path': '../lib'},
45+
},
46+
),
47+
]).create();
48+
49+
await pubGet(workingDirectory: p.join(d.sandbox, 'app_none'));
50+
await pubGet(workingDirectory: p.join(d.sandbox, 'app_hosted'));
51+
await pubGet(workingDirectory: p.join(d.sandbox, 'app_git'));
52+
await pubGet(workingDirectory: p.join(d.sandbox, 'app_path'));
53+
54+
final markingFiles =
55+
Directory(
56+
p.join(d.sandbox, cachePath, 'active_roots'),
57+
).listSync(recursive: true).whereType<File>().toList();
58+
59+
expect(markingFiles, hasLength(3));
60+
61+
for (final file in markingFiles) {
62+
final uri =
63+
(jsonDecode(file.readAsStringSync()) as Map)['package_config']
64+
as String;
65+
final hash = hexEncode(sha256.convert(utf8.encode(uri)).bytes);
66+
final hashFileName =
67+
'${p.basename(p.dirname(file.path))}${p.basename(file.path)}';
68+
expect(hashFileName, hash);
69+
}
70+
71+
expect(markingFiles, hasLength(3));
72+
73+
expect(SystemCache(rootDir: p.join(d.sandbox, cachePath)).activeRoots(), {
74+
p.canonicalize(
75+
p.join(d.sandbox, 'app_hosted', '.dart_tool', 'package_config.json'),
76+
),
77+
p.canonicalize(
78+
p.join(d.sandbox, 'app_git', '.dart_tool', 'package_config.json'),
79+
),
80+
81+
p.canonicalize(
82+
p.join(
83+
d.sandbox,
84+
cachePath,
85+
'global_packages',
86+
'foo',
87+
'.dart_tool',
88+
'package_config.json',
89+
),
90+
),
91+
});
92+
});
93+
}

test/embedding/embedding_test.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,13 @@ main() {
437437

438438
String _filter(String input) {
439439
return input
440-
.replaceAll(p.toUri(d.sandbox).toString(), r'file://$SANDBOX')
440+
.replaceAll(
441+
RegExp(
442+
RegExp.escape(p.toUri(d.sandbox).toString()),
443+
caseSensitive: false,
444+
),
445+
r'file://$SANDBOX',
446+
)
441447
.replaceAll(d.sandbox, r'$SANDBOX')
442448
.replaceAll(Platform.pathSeparator, '/')
443449
.replaceAll(Platform.operatingSystem, r'$OS')
@@ -546,6 +552,10 @@ String _filter(String input) {
546552
RegExp(r'"archive_sha256":"[0-9a-f]{64}"', multiLine: true),
547553
r'"archive_sha256":"$SHA256"',
548554
)
555+
.replaceAll(
556+
RegExp(r'active_roots/[0-9a-f]{2}/[0-9a-f]{62}', multiLine: true),
557+
r'active_roots/$HH/$HASH',
558+
)
549559
/// TODO(sigurdm): This hack suppresses differences in stack-traces
550560
/// between dart 2.17 and 2.18. Remove when 2.18 is stable.
551561
.replaceAllMapped(

test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ MSG : Logs written to $SANDBOX/cache/log/pub_log.txt.
149149
[E] | ],
150150
[E] | "configVersion": 1
151151
[E] | }
152+
[E] IO : Writing $N characters to text file $SANDBOX/cache/active_roots/$HH/$HASH.
153+
[E] FINE: Contents:
154+
[E] | {"package_config":"file://$SANDBOX/myapp/.dart_tool/package_config.json"}
152155
[E] IO : Writing $N characters to text file $SANDBOX/cache/log/pub_log.txt.
153156

154157
-------------------------------- END OF OUTPUT ---------------------------------
@@ -335,6 +338,9 @@ FINE: Contents:
335338
| ],
336339
| "configVersion": 1
337340
| }
341+
IO : Writing $N characters to text file $SANDBOX/cache/active_roots/$HH/$HASH.
342+
FINE: Contents:
343+
| {"package_config":"file://$SANDBOX/myapp/.dart_tool/package_config.json"}
338344
---- End log transcript ----
339345
-------------------------------- END OF OUTPUT ---------------------------------
340346

0 commit comments

Comments
 (0)