From 86df2d98ced9bafa04e1fcd0a7bf7df40658798e Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 8 Oct 2026 08:50:43 +0000 Subject: [PATCH 1/4] Verify cached packages' pubspec.yaml exists in isResolutionUpToDate Checking that pubspecPath exists before skipping cached packages ensures that missing cached dependencies trigger a re-resolution instead of crashing in Package.load. --- lib/src/entrypoint.dart | 6 ++--- test/deps_test.dart | 13 +++++++++ test/embedding/ensure_pubspec_resolved.dart | 30 ++++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 9e6c0f30510..d1197210097 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1321,15 +1321,15 @@ 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; } + if (p.isWithin(cache.rootDir, pubspecPath)) { + continue; + } if (!lockFileModified.isAfter(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); diff --git a/test/deps_test.dart b/test/deps_test.dart index 11dc0204616..525a7ed962c 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,18 @@ 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('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..7b4611c4a87 100644 --- a/test/embedding/ensure_pubspec_resolved.dart +++ b/test/embedding/ensure_pubspec_resolved.dart @@ -181,7 +181,35 @@ void testEnsurePubspecResolved() { // Ensure that the pubspec looks newer than the lockfile. await _touch('pubspec.yaml'); - await _implicitPubGet('`pubspec.yaml` is newer than `pubspec.lock`'); + final expectedPath = p.join( + r'$SANDBOX', + cachePath, + 'hosted', + r'localhost%58$PORT', + 'foo-1.0.0', + 'pubspec.yaml', + ); + await _implicitPubGet('Could not find `$expectedPath`'); + }); + + 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)); + + final expectedPath = p.join( + r'$SANDBOX', + cachePath, + 'hosted', + r'localhost%58$PORT', + 'foo-1.0.0', + 'pubspec.yaml', + ); + await _implicitPubGet('Could not find `$expectedPath`'); }); test('the package_config.json file points to the wrong place', () async { From 2004c3dd723ea7418623c3f714743f797545a1dc Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 8 Oct 2026 09:15:53 +0000 Subject: [PATCH 2/4] Use forward slashes in expected log path in ensure_pubspec_resolved --- test/embedding/ensure_pubspec_resolved.dart | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart index 7b4611c4a87..1e1c9d3f2fa 100644 --- a/test/embedding/ensure_pubspec_resolved.dart +++ b/test/embedding/ensure_pubspec_resolved.dart @@ -181,15 +181,9 @@ void testEnsurePubspecResolved() { // Ensure that the pubspec looks newer than the lockfile. await _touch('pubspec.yaml'); - final expectedPath = p.join( - r'$SANDBOX', - cachePath, - 'hosted', - r'localhost%58$PORT', - 'foo-1.0.0', - 'pubspec.yaml', + await _implicitPubGet( + r'Could not find `$SANDBOX/cache/hosted/localhost%58$PORT/foo-1.0.0/pubspec.yaml`', ); - await _implicitPubGet('Could not find `$expectedPath`'); }); test('a cached package was deleted', () async { @@ -201,15 +195,9 @@ void testEnsurePubspecResolved() { deleteEntry(p.join(d.sandbox, cachePath)); - final expectedPath = p.join( - r'$SANDBOX', - cachePath, - 'hosted', - r'localhost%58$PORT', - 'foo-1.0.0', - 'pubspec.yaml', + await _implicitPubGet( + r'Could not find `$SANDBOX/cache/hosted/localhost%58$PORT/foo-1.0.0/pubspec.yaml`', ); - await _implicitPubGet('Could not find `$expectedPath`'); }); test('the package_config.json file points to the wrong place', () async { From 121d90c7255656c74bb6afb798e3704b510e1f35 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Tue, 13 Oct 2026 06:50:54 +0000 Subject: [PATCH 3/4] Document why cached packages are stat'ed but not timestamp-checked Also replace the stale reference to the removed `_arePackagesAvailable` in `isPackageConfigUpToDate` with a description of why that check is still needed next to the one in the main loop. --- lib/src/entrypoint.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index d1197210097..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; @@ -1327,6 +1329,11 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // 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; } From ee614c3b974ba2d166ce053692c5389f224870fa Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Tue, 13 Oct 2026 07:05:13 +0000 Subject: [PATCH 4/4] Redownload a cached hosted package whose pubspec.yaml is missing `isResolutionUpToDate` now triggers a resolution when a cached package has no `pubspec.yaml`, but `HostedSource.downloadToSystemCache` only redownloaded a package when its directory was missing. With the directory present and the `pubspec.yaml` gone the implicit `pub get` therefore left the entry alone and `Package.load` failed with the same error as before, on every invocation. Treat such an entry as damaged and redownload it, like a wrong content hash. --- lib/src/source/hosted.dart | 10 ++++++++++ test/deps_test.dart | 12 ++++++++++++ test/get/hosted/get_test.dart | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+) 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 525a7ed962c..022e6781025 100644 --- a/test/deps_test.dart +++ b/test/deps_test.dart @@ -353,6 +353,18 @@ void main() { }, ); + 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/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); + }); }