Add a README.md to the pub cache after command ends (#3650)
diff --git a/lib/src/command/cache_clean.dart b/lib/src/command/cache_clean.dart index 42b0f1f..786707d 100644 --- a/lib/src/command/cache_clean.dart +++ b/lib/src/command/cache_clean.dart
@@ -32,8 +32,7 @@ You will have to run `$topLevelProgram pub get` again in each project. Are you sure?''')) { log.message('Removing pub cache directory ${cache.rootDir}.'); - deleteEntry(cache.rootDir); - ensureDir(cache.rootDir); + cache.clean(); } } else { log.message('No pub cache at ${cache.rootDir}.');
diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart index 54630c7..f847c87 100644 --- a/lib/src/source/cached.dart +++ b/lib/src/source/cached.dart
@@ -55,7 +55,10 @@ dirExists(getDirectoryInCache(id, cache)); /// Downloads the package identified by [id] to the system cache. - Future<PackageId> downloadToSystemCache(PackageId id, SystemCache cache); + Future<DownloadPackageResult> downloadToSystemCache( + PackageId id, + SystemCache cache, + ); /// Returns the [Package]s that have been downloaded to the system cache. List<Package> getCachedPackages(SystemCache cache); @@ -86,3 +89,14 @@ required this.success, }); } + +class DownloadPackageResult { + /// The resolved package. + final PackageId packageId; + + /// Whether we had to make changes in the cache in order to download the + /// package. + final bool didUpdate; + + DownloadPackageResult(this.packageId, {required this.didUpdate}); +}
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart index 24b1294..4bef7fa 100644 --- a/lib/src/source/git.dart +++ b/lib/src/source/git.dart
@@ -298,11 +298,12 @@ /// itself; each of the commit-specific directories are clones of a directory /// in `cache/`. @override - Future<PackageId> downloadToSystemCache( + Future<DownloadPackageResult> downloadToSystemCache( PackageId id, SystemCache cache, ) async { return await _pool.withResource(() async { + bool didUpdate = false; final ref = id.toRef(); final description = ref.description; if (description is! GitDescription) { @@ -317,7 +318,7 @@ final resolvedRef = (id.description as GitResolvedDescription).resolvedRef; - await _ensureRevision(ref, resolvedRef, cache); + didUpdate |= await _ensureRevision(ref, resolvedRef, cache); var revisionCachePath = _revisionCachePath(id, cache); final path = description.path; @@ -326,11 +327,12 @@ await _clone(_repoCachePath(ref, cache), revisionCachePath); await _checkOut(revisionCachePath, resolvedRef); _writePackageList(revisionCachePath, [path]); + didUpdate = true; } else { - _updatePackageList(revisionCachePath, path); + didUpdate |= _updatePackageList(revisionCachePath, path); } }); - return id; + return DownloadPackageResult(id, didUpdate: didUpdate); }); } @@ -425,13 +427,15 @@ /// Ensures that the canonical clone of the repository referred to by [ref] /// contains the given Git [revision]. - Future _ensureRevision( + /// + /// Returns `true` if it had to update anything. + Future<bool> _ensureRevision( PackageRef ref, String revision, SystemCache cache, ) async { var path = _repoCachePath(ref, cache); - if (_updatedRepos.contains(path)) return; + if (_updatedRepos.contains(path)) return false; await _deleteGitRepoIfInvalid(path); @@ -443,28 +447,33 @@ await _firstRevision(path, revision); } on git.GitException catch (_) { await _updateRepoCache(ref, cache); + return true; } + return false; } /// Ensures that the canonical clone of the repository referred to by [ref] /// exists and is up-to-date. - Future _ensureRepoCache(PackageRef ref, SystemCache cache) async { + /// + /// Returns `true` if it had to update anything. + Future<bool> _ensureRepoCache(PackageRef ref, SystemCache cache) async { var path = _repoCachePath(ref, cache); - if (_updatedRepos.contains(path)) return; + if (_updatedRepos.contains(path)) return false; await _deleteGitRepoIfInvalid(path); if (!entryExists(path)) { await _createRepoCache(ref, cache); + return true; } else { - await _updateRepoCache(ref, cache); + return await _updateRepoCache(ref, cache); } } /// Creates the canonical clone of the repository referred to by [ref]. /// /// This assumes that the canonical clone doesn't yet exist. - Future _createRepoCache(PackageRef ref, SystemCache cache) async { + Future<void> _createRepoCache(PackageRef ref, SystemCache cache) async { final description = ref.description; if (description is! GitDescription) { throw ArgumentError('Wrong source'); @@ -484,14 +493,17 @@ /// [ref]. /// /// This assumes that the canonical clone already exists. - Future _updateRepoCache( + /// + /// Returns `true` if it had to update anything. + Future<bool> _updateRepoCache( PackageRef ref, SystemCache cache, ) async { var path = _repoCachePath(ref, cache); - if (_updatedRepos.contains(path)) return Future.value(); + if (_updatedRepos.contains(path)) return false; await git.run(['fetch'], workingDir: path); _updatedRepos.add(path); + return true; } /// Clean-up [dirPath] if it's an invalid git repository. @@ -523,11 +535,14 @@ /// Updates the package list file in [revisionCachePath] to include [path], if /// necessary. - void _updatePackageList(String revisionCachePath, String path) { + /// + /// Returns `true` if it had to update anything. + bool _updatePackageList(String revisionCachePath, String path) { var packages = _readPackageList(revisionCachePath); - if (packages.contains(path)) return; + if (packages.contains(path)) return false; _writePackageList(revisionCachePath, packages..add(path)); + return true; } /// Returns the list of packages in [revisionCachePath]. @@ -571,7 +586,7 @@ /// /// If [shallow] is true, creates a shallow clone that contains no history /// for the repository. - Future _clone( + Future<void> _clone( String from, String to, { bool mirror = false, @@ -594,7 +609,7 @@ } /// Checks out the reference [ref] in [repoPath]. - Future _checkOut(String repoPath, String ref) { + Future<void> _checkOut(String repoPath, String ref) { return git .run(['checkout', ref], workingDir: repoPath).then((result) => null); }
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 1cb05d6..2d481e4 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -728,8 +728,9 @@ /// `pub get` with a filled cache to be a fast case that doesn't require any /// new version-listings. @override - Future<PackageId> downloadToSystemCache( + Future<DownloadPackageResult> downloadToSystemCache( PackageId id, SystemCache cache) async { + var didUpdate = false; final packageDir = getDirectoryInCache(id, cache); // Use the content-hash from the version-info to compare with what we @@ -779,17 +780,20 @@ if (dirExists(packageDir)) { contentHash ??= sha256FromCache(id, cache); } else { + didUpdate = true; if (cache.isOffline) { fail( 'Missing package ${id.name}-${id.version}. Try again without --offline.'); } contentHash = await _download(id, packageDir, cache); } - return PackageId( - id.name, - id.version, - (id.description as ResolvedHostedDescription).withSha256(contentHash), - ); + return DownloadPackageResult( + PackageId( + id.name, + id.version, + (id.description as ResolvedHostedDescription).withSha256(contentHash), + ), + didUpdate: didUpdate); } /// Determines if the package identified by [id] is already downloaded to the
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart index 11a10a1..2956626 100644 --- a/lib/src/system_cache.dart +++ b/lib/src/system_cache.dart
@@ -227,10 +227,21 @@ Future<PackageId> downloadPackage(PackageId id) async { final source = id.source; assert(source is CachedSource); - return await (source as CachedSource).downloadToSystemCache( + final result = await (source as CachedSource).downloadToSystemCache( id, this, ); + + // We only update the README.md in the cache when a change to the cache has + // happened. This is: + // * to avoid failing if used with a read-only cache, and + // * because the cost of writing a single file is negligible compared to + // downloading a package, but might be significant in the fast-case where + // a the cache is already valid. + if (result.didUpdate) { + _ensureReadme(); + } + return result.packageId; } /// Get the latest version of [package]. @@ -270,6 +281,51 @@ return latest; } + + /// Removes all contents of the system cache. + /// + /// Rewrites the README.md. + void clean() { + deleteEntry(rootDir); + ensureDir(rootDir); + _ensureReadme(); + } + + /// Write a README.md file in the root of the cache directory to document the + /// contents of the folder. + /// + /// This should only be called when we are doing another operation that is + /// modifying the `PUB_CACHE`. This ensures that users won't experience + /// permission errors because we writing a `README.md` file, in a flow that + /// the user expected wouldn't have issues with a read-only `PUB_CACHE`. + void _ensureReadme() { + /// We only want to do this once per run. + if (_hasEnsuredReadme) return; + _hasEnsuredReadme = true; + final readmePath = p.join(rootDir, 'README.md'); + try { + writeTextFile(readmePath, ''' +Pub Package Cache +================= + +This folder is used by Pub to store cached packages used in Dart / Flutter +projects. + +The contents of this folder should only be modified using the `dart pub` and +`flutter pub` commands. + +Modifying this folder manually can lead to inconsistent behavior. + +For details on how manage the `PUB_CACHE`, see: +https://dart.dev/go/pub-cache +'''); + } on Exception catch (e) { + // Failing to write the README.md should not disrupt other operations. + log.fine('Failed to write README.md in PUB_CACHE: $e'); + } + } + + bool _hasEnsuredReadme = false; } typedef SourceRegistry = Source Function(String? name);
diff --git a/test/cache/clean_test.dart b/test/cache/clean_test.dart index 7a79bc8..b5909b5 100644 --- a/test/cache/clean_test.dart +++ b/test/cache/clean_test.dart
@@ -23,11 +23,15 @@ await d.appDir({'foo': 'any', 'bar': 'any'}).create(); await pubGet(); final cache = path.join(d.sandbox, cachePath); - expect(listDir(cache, includeHidden: true), isNotEmpty); + expect(listDir(cache, includeHidden: true), contains(endsWith('hosted'))); await runPub( args: ['cache', 'clean', '--force'], output: 'Removing pub cache directory $cache.'); - expect(listDir(cache, includeHidden: true), isEmpty); + + expect( + listDir(cache, includeHidden: true), + // The README.md will be reconstructed. + [pathInCache('README.md')]); }); test('running pub cache clean deletes cache only with confirmation', @@ -38,7 +42,10 @@ await d.appDir({'foo': 'any', 'bar': 'any'}).create(); await pubGet(); final cache = path.join(d.sandbox, cachePath); - expect(listDir(cache, includeHidden: true), isNotEmpty); + expect( + listDir(cache, includeHidden: true), + contains(pathInCache('hosted')), + ); { final process = await startPub( args: ['cache', 'clean'], @@ -46,7 +53,10 @@ process.stdin.writeln('n'); expect(await process.exitCode, 0); } - expect(listDir(cache, includeHidden: true), isNotEmpty); + expect( + listDir(cache, includeHidden: true), + contains(pathInCache('hosted')), + ); { final process = await startPub( @@ -55,6 +65,9 @@ process.stdin.writeln('y'); expect(await process.exitCode, 0); } - expect(listDir(cache, includeHidden: true), isEmpty); + expect( + listDir(cache, + includeHidden: true), // The README.md will be reconstructed. + [pathInCache('README.md')]); }); }
diff --git a/test/cache/create_readme_test.dart b/test/cache/create_readme_test.dart new file mode 100644 index 0000000..035fd68 --- /dev/null +++ b/test/cache/create_readme_test.dart
@@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() async { + test('PUB_CACHE/README.md gets created by command downloading to pub cache', + () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + await d.appDir().create(); + await pubGet(); + await d.nothing(cachePath).validate(); + + await d.appDir({'foo': '1.0.0'}).create(); + await pubGet(); + await d.dir(cachePath, [ + d.file('README.md', contains('https://dart.dev/go/pub-cache')) + ]).validate(); + File(pathInCache('README.md')).deleteSync(); + // No new download, so 'README.md' doesn't get updated. + await pubGet(); + await d.dir(cachePath, [d.nothing('README.md')]).validate(); + }); + + test('PUB_CACHE/README.md gets created by `dart pub cache clean`', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + await d.appDir({'foo': '1.0.0'}).create(); + await pubGet(); + await d.dir(cachePath, [ + d.file('README.md', contains('https://dart.dev/go/pub-cache')) + ]).validate(); + }); +}
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart index 4419c15..89b4d08 100644 --- a/test/embedding/embedding_test.dart +++ b/test/embedding/embedding_test.dart
@@ -99,7 +99,7 @@ d.dir('bin', [ d.file('main.dart', ''' import 'dart:io'; -main() { +main() { print('Hi'); exit(123); }
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 219820e..f48d6ba 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
@@ -72,6 +72,21 @@ [E] FINE: Extracted .tar.gz to $DIR [E] IO : Renaming directory $A to $B [E] IO : Deleting directory $DIR +[E] IO : Writing $N characters to text file $SANDBOX/cache/README.md. +[E] FINE: Contents: +[E] | Pub Package Cache +[E] | ================= +[E] | +[E] | This folder is used by Pub to store cached packages used in Dart / Flutter +[E] | projects. +[E] | +[E] | The contents of this folder should only be modified using the `dart pub` and +[E] | `flutter pub` commands. +[E] | +[E] | Modifying this folder manually can lead to inconsistent behavior. +[E] | +[E] | For details on how manage the `PUB_CACHE`, see: +[E] | https://dart.dev/go/pub-cache [E] IO : Writing $N characters to text file pubspec.lock. [E] FINE: Contents: [E] | # Generated by pub @@ -216,6 +231,21 @@ FINE: Extracted .tar.gz to $DIR IO : Renaming directory $A to $B IO : Deleting directory $DIR +IO : Writing $N characters to text file $SANDBOX/cache/README.md. +FINE: Contents: + | Pub Package Cache + | ================= + | + | This folder is used by Pub to store cached packages used in Dart / Flutter + | projects. + | + | The contents of this folder should only be modified using the `dart pub` and + | `flutter pub` commands. + | + | Modifying this folder manually can lead to inconsistent behavior. + | + | For details on how manage the `PUB_CACHE`, see: + | https://dart.dev/go/pub-cache MSG : + foo 1.0.0 IO : Writing $N characters to text file pubspec.lock. FINE: Contents: