Mark package-configs as active when writing (#4592)
diff --git a/doc/cache_layout.md b/doc/cache_layout.md
index 47c6305..3347a2f 100644
--- a/doc/cache_layout.md
+++ b/doc/cache_layout.md
@@ -34,6 +34,7 @@
```plaintext
$PUB_CACHE/
├── global_packages/ # Globally activated packages
+├── active_roots/ # Information about packages that this cache caches for.
├── bin/ # Executables compiled from globally activated packages.
├── git/ # Cloned git packages
├── hosted/ # Hosted package downloads
@@ -233,9 +234,30 @@
└── stagehand
```
+# Active roots
+$PUB_CACHE/active_roots/
+
+In order to be able to prune the cache (`dart pub cache gc`) pub keeps a tally of
+each time it writes a `.dart_tool/package_config.json` file (an activation).
+
+The directory is laid out such that each file-name is the hex-encoded sha256
+hash of the absolute file-uri of the path of the package config.
+
+The first two bytes are used for a subdirectory, to prevent too many files in
+one directory.
+
+When implemented `dart pub cache gc` will look through all the package configs,
+and mark all cached packages in the cache used by those projects `alive`. If a
+package config doesn't exist, it is ignored, and the file marking it is deleted.
+
+All other packages in the cache are removed.
+
+Packages that are installed in the cache within 1 day are not deleted. This is
+to minimize the risk of race-conditions.
+
## Logs
When pub crashes or is run with `--verbose` it will create a
`$PUB_CACHE/log/pub_log.txt` with the dart sdk version, platform, `$PUB_CACHE`,
-`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log and
-stack-trace.
+`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log
+and stack-trace.
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 43e3713..1de6570 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -402,6 +402,8 @@
/// If the workspace is non-trivial: For each package in the workspace write:
/// `.dart_tool/pub/workspace_ref.json` with a pointer to the workspace root
/// package dir.
+ ///
+ /// Also marks the package active in `PUB_CACHE/active_roots/`.
Future<void> writePackageConfigFiles() async {
ensureDir(p.dirname(packageConfigPath));
@@ -433,6 +435,9 @@
writeTextFileIfDifferent(workspaceRefPath, '$workspaceRef\n');
}
}
+ if (lockFile.packages.values.any((id) => id.source is CachedSource)) {
+ cache.markRootActive(packageConfigPath);
+ }
}
Future<String> _packageGraphFile(SystemCache cache) async {
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 9332447..25169eb 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -293,10 +293,14 @@
lockFile.writeToFile(p.join(tempDir, 'pubspec.lock'), cache);
+ final packageDir = _packageDir(name);
+ tryDeleteEntry(packageDir);
+ tryRenameDir(tempDir, packageDir);
+
// Load the package graph from [result] so we don't need to re-parse all
// the pubspecs.
final entrypoint = Entrypoint.global(
- root,
+ packageForConstraint(dep, packageDir),
lockFile,
cache,
solveResult: result,
@@ -305,9 +309,6 @@
await entrypoint.writePackageConfigFiles();
await entrypoint.precompileExecutables();
-
- tryDeleteEntry(_packageDir(name));
- tryRenameDir(tempDir, _packageDir(name));
}
final entrypoint = Entrypoint.global(
diff --git a/lib/src/io.dart b/lib/src/io.dart
index b7d02ae..fd9f646 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -257,6 +257,10 @@
File(file).writeAsStringSync(contents, encoding: encoding);
}
+/// Reads the file at [path] and writes [newContent] to it, if it is different
+/// from [newContent].
+///
+/// If the file doesn't exist it is always written.
void writeTextFileIfDifferent(String path, String newContent) {
// Compare to the present package_config.json
// For purposes of equality we don't care about the `generated` timestamp.
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index bf52180..90735e7 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -2,8 +2,10 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:convert';
import 'dart:io';
+import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
@@ -80,6 +82,8 @@
Source get defaultSource => hosted;
+ late final Iterable<CachedSource> cachedSources = [hosted, git];
+
/// The built-in Git source.
GitSource get git => GitSource.instance;
@@ -400,6 +404,74 @@
}
bool _hasMaintainedCache = false;
+
+ late final _activeRootsDir = p.join(rootDir, 'active_roots');
+
+ /// Returns the paths of all packages_configs registered in
+ /// [_activeRootsDir].
+ List<String> activeRoots() {
+ final List<String> files;
+ try {
+ files = listDir(_activeRootsDir, includeDirs: false, recursive: true);
+ } on IOException {
+ return [];
+ }
+ final activeRoots = <String>[];
+ for (final file in files) {
+ final Object? decoded;
+ try {
+ decoded = jsonDecode(readTextFile(file));
+ } on IOException catch (e) {
+ log.fine('Could not read $file $e - deleting');
+ tryDeleteEntry(file);
+ continue;
+ } on FormatException catch (e) {
+ log.fine('Could not decode $file $e - deleting');
+ tryDeleteEntry(file);
+ continue;
+ }
+ if (decoded is! Map<String, Object?>) {
+ log.fine('Faulty $file - deleting');
+ tryDeleteEntry(file);
+ continue;
+ }
+ final uriText = decoded['package_config'];
+ if (uriText is! String) {
+ log.fine('Faulty $file - deleting');
+ tryDeleteEntry(file);
+ continue;
+ }
+ final uri = Uri.tryParse(uriText);
+ if (uri == null || !uri.isScheme('file')) {
+ log.fine('Faulty $file - deleting');
+ tryDeleteEntry(file);
+ continue;
+ }
+ activeRoots.add(uri.toFilePath());
+ }
+ return activeRoots;
+ }
+
+ /// Adds a file to the `PUB_CACHE/active_roots/` dir indicating
+ /// [packageConfigPath] is active.
+ void markRootActive(String packageConfigPath) {
+ final canonicalFileUri =
+ p.toUri(p.canonicalize(packageConfigPath)).toString();
+
+ final hash = hexEncode(sha256.convert(utf8.encode(canonicalFileUri)).bytes);
+
+ final firstTwo = hash.substring(0, 2);
+ final theRest = hash.substring(2);
+
+ final dir = p.join(_activeRootsDir, firstTwo);
+ ensureDir(dir);
+
+ final filename = p.join(dir, theRest);
+ writeTextFileIfDifferent(
+ filename,
+ '${jsonEncode({'package_config': canonicalFileUri})}\n',
+ );
+ }
}
typedef SourceRegistry = Source Function(String? name);
diff --git a/test/cache/gc_test.dart b/test/cache/gc_test.dart
new file mode 100644
index 0000000..a56944e
--- /dev/null
+++ b/test/cache/gc_test.dart
@@ -0,0 +1,93 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:crypto/crypto.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub/src/system_cache.dart';
+import 'package:pub/src/utils.dart';
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() async {
+ test('marks a package active on pub get and global activate', () async {
+ final server = await servePackages();
+
+ server.serve('foo', '1.0.0');
+ server.serve('bar', '1.0.0');
+
+ await runPub(args: ['global', 'activate', 'foo']);
+
+ // Without cached dependencies we don't register the package
+ await d.dir('app_none', [d.appPubspec()]).create();
+
+ await d.appDir(dependencies: {'bar': '1.0.0'}).create();
+
+ await d.dir('app_hosted', [
+ d.appPubspec(dependencies: {'bar': '^1.0.0'}),
+ ]).create();
+
+ await d.git('lib', [d.libPubspec('lib', '1.0.0')]).create();
+
+ await d.dir('app_git', [
+ d.appPubspec(
+ dependencies: {
+ 'lib': {'git': '../lib'},
+ },
+ ),
+ ]).create();
+
+ await d.dir('app_path', [
+ d.appPubspec(
+ dependencies: {
+ 'lib': {'path': '../lib'},
+ },
+ ),
+ ]).create();
+
+ await pubGet(workingDirectory: p.join(d.sandbox, 'app_none'));
+ await pubGet(workingDirectory: p.join(d.sandbox, 'app_hosted'));
+ await pubGet(workingDirectory: p.join(d.sandbox, 'app_git'));
+ await pubGet(workingDirectory: p.join(d.sandbox, 'app_path'));
+
+ final markingFiles =
+ Directory(
+ p.join(d.sandbox, cachePath, 'active_roots'),
+ ).listSync(recursive: true).whereType<File>().toList();
+
+ expect(markingFiles, hasLength(3));
+
+ for (final file in markingFiles) {
+ final uri =
+ (jsonDecode(file.readAsStringSync()) as Map)['package_config']
+ as String;
+ final hash = hexEncode(sha256.convert(utf8.encode(uri)).bytes);
+ final hashFileName =
+ '${p.basename(p.dirname(file.path))}${p.basename(file.path)}';
+ expect(hashFileName, hash);
+ }
+
+ expect(markingFiles, hasLength(3));
+
+ expect(SystemCache(rootDir: p.join(d.sandbox, cachePath)).activeRoots(), {
+ p.canonicalize(
+ p.join(d.sandbox, 'app_hosted', '.dart_tool', 'package_config.json'),
+ ),
+ p.canonicalize(
+ p.join(d.sandbox, 'app_git', '.dart_tool', 'package_config.json'),
+ ),
+
+ p.canonicalize(
+ p.join(
+ d.sandbox,
+ cachePath,
+ 'global_packages',
+ 'foo',
+ '.dart_tool',
+ 'package_config.json',
+ ),
+ ),
+ });
+ });
+}
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index db63269..83176d8 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -437,7 +437,13 @@
String _filter(String input) {
return input
- .replaceAll(p.toUri(d.sandbox).toString(), r'file://$SANDBOX')
+ .replaceAll(
+ RegExp(
+ RegExp.escape(p.toUri(d.sandbox).toString()),
+ caseSensitive: false,
+ ),
+ r'file://$SANDBOX',
+ )
.replaceAll(d.sandbox, r'$SANDBOX')
.replaceAll(Platform.pathSeparator, '/')
.replaceAll(Platform.operatingSystem, r'$OS')
@@ -546,6 +552,10 @@
RegExp(r'"archive_sha256":"[0-9a-f]{64}"', multiLine: true),
r'"archive_sha256":"$SHA256"',
)
+ .replaceAll(
+ RegExp(r'active_roots/[0-9a-f]{2}/[0-9a-f]{62}', multiLine: true),
+ r'active_roots/$HH/$HASH',
+ )
/// TODO(sigurdm): This hack suppresses differences in stack-traces
/// between dart 2.17 and 2.18. Remove when 2.18 is stable.
.replaceAllMapped(
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index 41f85c0..21b203d 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -149,6 +149,9 @@
[E] | ],
[E] | "configVersion": 1
[E] | }
+[E] IO : Writing $N characters to text file $SANDBOX/cache/active_roots/$HH/$HASH.
+[E] FINE: Contents:
+[E] | {"package_config":"file://$SANDBOX/myapp/.dart_tool/package_config.json"}
[E] IO : Writing $N characters to text file $SANDBOX/cache/log/pub_log.txt.
-------------------------------- END OF OUTPUT ---------------------------------
@@ -335,6 +338,9 @@
| ],
| "configVersion": 1
| }
+IO : Writing $N characters to text file $SANDBOX/cache/active_roots/$HH/$HASH.
+FINE: Contents:
+ | {"package_config":"file://$SANDBOX/myapp/.dart_tool/package_config.json"}
---- End log transcript ----
-------------------------------- END OF OUTPUT ---------------------------------