diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 9e6c0f30510..0101bfcbc61 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1046,8 +1046,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without } // For cached sources, make sure the directory exists and looks like a - // package. This is also done by [_arePackagesAvailable] but that may - // not be run if the lockfile is newer than the pubspec. + // package. The main loop in [isResolutionUpToDate] also checks that + // the `pubspec.yaml` exists, but it stops early at the first pubspec + // that is strictly newer than the lockfile, so it may not have + // reached this package. if (source is CachedSource && !dirExists(lockFilePackagePath) || !fileExists(p.join(lockFilePackagePath, 'pubspec.yaml'))) { return false; @@ -1321,15 +1323,20 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without 'pubspec.yaml', ), ); - if (p.isWithin(cache.rootDir, pubspecPath)) { - continue; - } final pubspecStat = tryStatFile(pubspecPath); if (pubspecStat == null) { log.fine('Could not find `$pubspecPath`'); // A dependency is missing - do a full new resolution. return null; } + // Cached packages are immutable, so their timestamps say nothing about + // this resolution: an entry in a shared cache may well have been + // downloaded by another project after this lockfile was written. They do + // have to exist though, which is why they are only skipped after the + // stat above. See https://github.com/dart-lang/pub/issues/4559. + if (p.isWithin(cache.rootDir, pubspecPath)) { + continue; + } if (!lockFileModified.isAfter(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index e3a641f3c07..8f9d98d5722 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -1306,6 +1306,16 @@ class HostedSource extends CachedSource { contentHash = hashFromCache; } } + if (dirExists(packageDir) && + !fileExists(p.join(packageDir, 'pubspec.yaml'))) { + // The entry is unusable without a `pubspec.yaml`. Pub never extracts a + // package without one, so something else has damaged the cache. + log.warning( + 'Cached version of ${id.name}-${id.version} is missing its ' + 'pubspec.yaml - redownloading.', + ); + deleteEntry(packageDir); + } if (dirExists(packageDir)) { contentHash ??= sha256FromCache(id, cache); } else { diff --git a/test/deps_test.dart b/test/deps_test.dart index 11dc0204616..022e6781025 100644 --- a/test/deps_test.dart +++ b/test/deps_test.dart @@ -5,6 +5,7 @@ @TestOn('vm') library; +import 'package:pub/src/io.dart'; import 'package:pub/src/path.dart'; import 'package:test/test.dart'; @@ -340,6 +341,30 @@ void main() { ); }); + test( + 're-resolves and succeeds when a cached package is deleted with --json', + () async { + await pubGet(); + deleteEntry(p.join(d.sandbox, cachePath)); + await runPub( + args: ['deps', '--json'], + output: contains('"name": "normal"'), + ); + }, + ); + + test("re-resolves and succeeds when a cached package's pubspec.yaml is " + 'deleted with --json', () async { + await pubGet(); + deleteEntry( + p.join(d.sandbox, d.hostedCachePath(), 'normal-1.2.3', 'pubspec.yaml'), + ); + await runPub( + args: ['deps', '--json'], + output: contains('"name": "normal"'), + ); + }); + test('with the Flutter SDK, if applicable', () async { await pubGet(); diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart index c3448b8ac3b..1e1c9d3f2fa 100644 --- a/test/embedding/ensure_pubspec_resolved.dart +++ b/test/embedding/ensure_pubspec_resolved.dart @@ -181,7 +181,23 @@ void testEnsurePubspecResolved() { // Ensure that the pubspec looks newer than the lockfile. await _touch('pubspec.yaml'); - await _implicitPubGet('`pubspec.yaml` is newer than `pubspec.lock`'); + await _implicitPubGet( + r'Could not find `$SANDBOX/cache/hosted/localhost%58$PORT/foo-1.0.0/pubspec.yaml`', + ); + }); + + test('a cached package was deleted', () async { + await d.dir(appPath, [ + d.appPubspec(dependencies: {'foo': '1.0.0'}), + ]).create(); + + await pubGet(); + + deleteEntry(p.join(d.sandbox, cachePath)); + + await _implicitPubGet( + r'Could not find `$SANDBOX/cache/hosted/localhost%58$PORT/foo-1.0.0/pubspec.yaml`', + ); }); test('the package_config.json file points to the wrong place', () async { diff --git a/test/get/hosted/get_test.dart b/test/get/hosted/get_test.dart index 899257d1295..1c800d45ae5 100644 --- a/test/get/hosted/get_test.dart +++ b/test/get/hosted/get_test.dart @@ -444,4 +444,26 @@ void main() { exitCode: UNAVAILABLE, ); }); + + test('redownloads a cached package whose pubspec.yaml is missing', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + await d.appDir(dependencies: {'foo': '1.0.0'}).create(); + await pubGet(); + + final pubspecPath = p.join( + d.sandbox, + d.hostedCachePath(), + 'foo-1.0.0', + 'pubspec.yaml', + ); + deleteEntry(pubspecPath); + + await pubGet( + warning: + 'Cached version of foo-1.0.0 is missing its pubspec.yaml ' + '- redownloading.', + ); + expect(fileExists(pubspecPath), isTrue); + }); }