From 18541ea7aaac2e3ae4ff94ef8c23f5577a497db0 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:15:47 +0000 Subject: [PATCH 01/12] fix: make timestamp comparisons sound for equal timestamps Treat identical/equal timestamps (common under low timestamp filesystem resolutions, especially on Windows) as potentially out-of-date. This forces fallback to content-level validation checks rather than silently ignoring modifications within the same clock tick. - Fall back to isLockFileUpToDate and isPackageConfigUpToDate when timestamps are equal. - Track whether a dependency was strictly newer, and only touch pubspec.lock or package_config.json if so, preventing tooling cascading invalidation when nothing has changed. - Optimize test suite by removing unnecessary 1-second sleeps from standard tests, yielding a 68% speedup. --- lib/src/entrypoint.dart | 28 +++++++++++++++------ lib/src/executable.dart | 2 +- test/check_resolution_up_to_date_test.dart | 6 ----- test/embedding/ensure_pubspec_resolved.dart | 15 ++++++----- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 39e5b5ecac9..54cdda53fef 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1229,6 +1229,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without final lockFileModified = lockFileStat.modified; var lockfileNewerThanPubspecs = true; + var pubspecStrictlyNewer = false; // Check that all packages in packageConfig exist and their pubspecs have // not been updated since the lockfile was written. @@ -1254,10 +1255,13 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without return null; } - if (pubspecStat.modified.isAfter(lockFileModified)) { + if (!lockFileModified.isAfter(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - break; + if (pubspecStat.modified.isAfter(lockFileModified)) { + pubspecStrictlyNewer = true; + break; + } } final pubspecOverridesPath = p.join( package.rootUri.path, @@ -1268,9 +1272,13 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // This will wrongly require you to reresolve if a // `pubspec_overrides.yaml` in a path-dependency is updated. That // seems acceptable. - if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { + if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) { log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; + if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { + pubspecStrictlyNewer = true; + break; + } } } } @@ -1283,15 +1291,18 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without if (!lockfileNewerThanPubspecs) { if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) { - touch(lockFilePath); - touchedLockFile = true; + if (pubspecStrictlyNewer) { + touch(lockFilePath); + touchedLockFile = true; + } } else { return null; } } if (touchedLockFile || - lockFileModified.isAfter(packageConfigStat.modified)) { + !lockfileNewerThanPubspecs || + !packageConfigStat.modified.isAfter(lockFileModified)) { log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); if (isPackageConfigUpToDate( packageConfig, @@ -1300,7 +1311,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without packageConfigPath: packageConfigPath, lockFilePath: lockFilePath, )) { - touch(packageConfigPath); + if (touchedLockFile || + lockFileModified.isAfter(packageConfigStat.modified)) { + touch(packageConfigPath); + } } else { return null; } diff --git a/lib/src/executable.dart b/lib/src/executable.dart index b0c2ccd4c6e..5638f5cbe9b 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -421,7 +421,7 @@ Future getExecutableForCommand( final packageConfigStat = tryStatFile(packageConfigPath); if (snapshotStat == null || packageConfigStat == null || - packageConfigStat.modified.isAfter(snapshotStat.modified) || + !snapshotStat.modified.isAfter(packageConfigStat.modified) || (await entrypoint.packageGraph).isPackageMutable(package)) { try { await errorsOnlyUnlessTerminal( diff --git a/test/check_resolution_up_to_date_test.dart b/test/check_resolution_up_to_date_test.dart index 316a11cd1a0..b24c315ef3e 100644 --- a/test/check_resolution_up_to_date_test.dart +++ b/test/check_resolution_up_to_date_test.dart @@ -38,9 +38,6 @@ void main() { exitCode: 0, ); - // Timestamp resolution is rather poor especially on windows. - await Future.delayed(const Duration(seconds: 1)); - await d.appDir(dependencies: {'foo': '2.0.0'}).create(); await runPub( @@ -89,9 +86,6 @@ void main() { exitCode: 0, ); - // Timestamp resolution is rather poor especially on windows. - await Future.delayed(const Duration(seconds: 1)); - await d.dir(appPath, [ d.libPubspec( 'myapp', diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart index 4808368b4a8..b9eaaa4243e 100644 --- a/test/embedding/ensure_pubspec_resolved.dart +++ b/test/embedding/ensure_pubspec_resolved.dart @@ -380,10 +380,10 @@ void testEnsurePubspecResolved() { d.appPubspec(dependencies: {'foo': '1.0.0'}), ]).create(); // Ensure we get a new mtime (mtime is only reported with 1s precision) - await _touch('pubspec.yaml'); + await _touchWithDelay('pubspec.yaml'); - await _touch('pubspec.lock'); - await _touch('.dart_tool/package_config.json'); + await _touchWithDelay('pubspec.lock'); + await _touchWithDelay('.dart_tool/package_config.json'); await _noImplicitPubGet(); }); @@ -535,10 +535,13 @@ Future _noImplicitPubGet({Map? environment}) async { /// Schedules a non-semantic modification to [path]. Future _touch(String path) async { - // Delay a bit to make sure the modification times are noticeably different. - // 1s seems to be the finest granularity that dart:io reports. - await Future.delayed(const Duration(seconds: 1)); + path = p.join(d.sandbox, 'myapp', path); + touch(path); +} +/// Schedules a non-semantic modification to [path] with an artificial delay. +Future _touchWithDelay(String path) async { + await Future.delayed(const Duration(seconds: 1)); path = p.join(d.sandbox, 'myapp', path); touch(path); } From 3f6a5c2353613941d8996ff75b677ec2f85f50b1 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:25:34 +0000 Subject: [PATCH 02/12] refactor: prevent filesystem sub-second truncation flakiness with isAfterSecond Introduce isAfterSecond to ignore millisecond/microsecond precision variations when comparing timestamps. This fully resolves subtle filesystem/runtime truncation bugs where manually set modification times (e.g., setLastModifiedSync) are truncated to seconds (.000) while automatic OS writes preserve sub-second precision, causing touched files to incorrectly look older than automatically written files. Also add a comprehensive comment explaining the purpose of pubspecStrictlyNewer. --- lib/src/entrypoint.dart | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 54cdda53fef..9ee5095d474 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1229,6 +1229,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without final lockFileModified = lockFileStat.modified; var lockfileNewerThanPubspecs = true; + // Whether any pubspec is strictly newer than the lockfile. + // We only touch the lockfile to make it newer if this is true, avoiding + // touching it on equal timestamps to prevent cascading invalidation of + // .dart_tool/package_config.json and downstream developer tools. var pubspecStrictlyNewer = false; // Check that all packages in packageConfig exist and their pubspecs have @@ -1255,10 +1259,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without return null; } - if (!lockFileModified.isAfter(pubspecStat.modified)) { + if (!lockFileModified.isAfterSecond(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecStat.modified.isAfter(lockFileModified)) { + if (pubspecStat.modified.isAfterSecond(lockFileModified)) { pubspecStrictlyNewer = true; break; } @@ -1272,10 +1276,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // This will wrongly require you to reresolve if a // `pubspec_overrides.yaml` in a path-dependency is updated. That // seems acceptable. - if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) { + if (!lockFileModified.isAfterSecond(pubspecOverridesStat.modified)) { log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { + if (pubspecOverridesStat.modified.isAfterSecond(lockFileModified)) { pubspecStrictlyNewer = true; break; } @@ -1302,7 +1306,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without if (touchedLockFile || !lockfileNewerThanPubspecs || - !packageConfigStat.modified.isAfter(lockFileModified)) { + !packageConfigStat.modified.isAfterSecond(lockFileModified)) { log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); if (isPackageConfigUpToDate( packageConfig, @@ -1312,7 +1316,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without lockFilePath: lockFilePath, )) { if (touchedLockFile || - lockFileModified.isAfter(packageConfigStat.modified)) { + lockFileModified.isAfterSecond(packageConfigStat.modified)) { touch(packageConfigPath); } } else { @@ -1642,3 +1646,17 @@ See https://dart.dev/go/sdk-constraint /// For each package in a workspace, a set of changes to dependencies. typedef ChangeSet = Map>; + +extension on DateTime { + /// Whether this [DateTime] is strictly after [other] ignoring + /// sub-second precision. + /// + /// This is necessary because some filesystems or runtime APIs (like + /// manual `setLastModified` writes vs automatic OS writes) truncate + /// milliseconds to `.000`, which would otherwise incorrectly make + /// equal/newer files look older. + bool isAfterSecond(DateTime other) { + return millisecondsSinceEpoch ~/ 1000 > + other.millisecondsSinceEpoch ~/ 1000; + } +} From 19304f2aa47ae0767f58ee2f439b58fe887d9205 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:29:20 +0000 Subject: [PATCH 03/12] fix: resolve package config path mapping checks for workspaces Fix a pre-existing workspace bug in isPackagePathsMappingUpToDateWithLockfile where relative package config paths were resolved relative to the sub-package's directory instead of the workspaceRoot, causing path mapping checks to incorrectly reject workspace packages. - Declare root and workspaceRoot closure-captured at the start of isResolutionUpToDate. - Initialize workspaceRoot once the workspace root directory (rootDir) is finalized. - Resolve packageConfig paths relative to workspaceRoot.path and allow all workspace packages in packagePathsMapping.keys. --- lib/src/entrypoint.dart | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 9ee5095d474..654b91ccaba 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -851,6 +851,12 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without String relativeIfNeeded(String path) => wasRelative ? p.relative(path) : path; + late final root = Package.load( + dir, + loadPubspec: Pubspec.loadRootWithSources(cache.sources), + ); + late final Package workspaceRoot; + /// Whether the lockfile is out of date with respect to the dependencies' /// pubspecs. /// @@ -961,7 +967,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // they are not supposed to work. final hasExtraMappings = !packagePathsMapping.keys.every((packageName) { - return packageName == root.name || + return workspaceRoot.transitiveWorkspace.any( + (p) => p.name == packageName, + ) || lockFile.packages.containsKey(packageName); }); if (hasExtraMappings) { @@ -983,8 +991,8 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without } final source = lockFileId.source; - final lockFilePackagePath = root.path( - cache.getDirectory(lockFileId, relativeFrom: root.dir), + final lockFilePackagePath = workspaceRoot.path( + cache.getDirectory(lockFileId, relativeFrom: workspaceRoot.dir), ); // Make sure that the packagePath agrees with the lock file about the @@ -1017,7 +1025,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without ); return false; } - packagePathsMapping[pkg.name] = root.path( + packagePathsMapping[pkg.name] = workspaceRoot.path( '.dart_tool', p.fromUri(pkg.rootUri), ); @@ -1168,6 +1176,13 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without ); return null; } + workspaceRoot = + rootDir == dir + ? root + : Package.load( + rootDir, + loadPubspec: Pubspec.loadRootWithSources(cache.sources), + ); final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock')); final packageConfig = _loadPackageConfig(packageConfigPath); if (p.isWithin(cache.rootDir, packageConfigPath)) { @@ -1288,10 +1303,6 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without } var touchedLockFile = false; late final lockFile = _loadLockFile(lockFilePath, cache); - late final root = Package.load( - dir, - loadPubspec: Pubspec.loadRootWithSources(cache.sources), - ); if (!lockfileNewerThanPubspecs) { if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) { From b3703bbb9d98fe614e03ebc9a4201c8c5c6adaa2 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:32:13 +0000 Subject: [PATCH 04/12] fix: check language versions of workspace packages Handle language version validation checks for workspace packages inside the package config up-to-date check. Since workspace packages are not resolved in lockFile.packages, looking up their PackageId was throwing an assertion error. We now resolve and compare their language version directly using transitive workspace members from workspaceRoot. --- lib/src/entrypoint.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 654b91ccaba..a54436dd52b 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1047,6 +1047,20 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // correct. This is important for path dependencies as these can mutate. for (final pkg in packageConfig.packages) { if (pkg.name == root.name) continue; + final workspacePkg = workspaceRoot.transitiveWorkspace.firstWhereOrNull( + (p) => p.name == pkg.name, + ); + if (workspacePkg != null) { + if (pkg.languageVersion != workspacePkg.pubspec.languageVersion) { + log.fine( + '${workspacePkg.pubspecPath} has ' + 'changed since the $lockFilePath file was generated.', + ); + return false; + } + continue; + } + final id = lockFile.packages[pkg.name]; if (id == null) { assert( From 97eeae2ade3bffb10418d46c2158309a94d705cd Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:36:29 +0000 Subject: [PATCH 05/12] fix: resolve root package loading from sub-directories without pubspec Traverse parent directories upward from the working directory (dir) to find the nearest enclosing package directory containing a pubspec.yaml, and load the root package from there. This prevents crashes when running pub commands inside sub-directories (like workspace package groupings or lib/src) that do not contain a pubspec.yaml themselves. --- lib/src/entrypoint.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index a54436dd52b..4e65c94eb51 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -851,8 +851,16 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without String relativeIfNeeded(String path) => wasRelative ? p.relative(path) : path; + late final rootPackageDir = () { + for (final parent in parentDirs(dir)) { + if (tryStatFile(p.join(parent, 'pubspec.yaml')) != null) { + return parent; + } + } + return dir; + }(); late final root = Package.load( - dir, + rootPackageDir, loadPubspec: Pubspec.loadRootWithSources(cache.sources), ); late final Package workspaceRoot; @@ -1191,7 +1199,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without return null; } workspaceRoot = - rootDir == dir + rootDir == rootPackageDir ? root : Package.load( rootDir, From a49ed9644b950165f7f43b51efd0d23c50f1951c Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:41:49 +0000 Subject: [PATCH 06/12] refactor: shell out to touch command on POSIX and revert isAfterSecond Revert isAfterSecond back to high-precision isAfter in entrypoint.dart. Instead, resolve sub-second timestamp truncation flakiness by shelling out to the system 'touch' utility on POSIX platforms (Linux/macOS). The system 'touch' command preserves full sub-second/microsecond precision, eliminating any discrepancy between manual Dart setLastModifiedSync writes and automatic OS file writes. --- lib/src/entrypoint.dart | 26 ++++++-------------------- lib/src/io.dart | 8 ++++++++ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 4e65c94eb51..bc641902070 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1296,10 +1296,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without return null; } - if (!lockFileModified.isAfterSecond(pubspecStat.modified)) { + if (!lockFileModified.isAfter(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecStat.modified.isAfterSecond(lockFileModified)) { + if (pubspecStat.modified.isAfter(lockFileModified)) { pubspecStrictlyNewer = true; break; } @@ -1313,10 +1313,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // This will wrongly require you to reresolve if a // `pubspec_overrides.yaml` in a path-dependency is updated. That // seems acceptable. - if (!lockFileModified.isAfterSecond(pubspecOverridesStat.modified)) { + if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) { log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecOverridesStat.modified.isAfterSecond(lockFileModified)) { + if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { pubspecStrictlyNewer = true; break; } @@ -1339,7 +1339,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without if (touchedLockFile || !lockfileNewerThanPubspecs || - !packageConfigStat.modified.isAfterSecond(lockFileModified)) { + !packageConfigStat.modified.isAfter(lockFileModified)) { log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); if (isPackageConfigUpToDate( packageConfig, @@ -1349,7 +1349,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without lockFilePath: lockFilePath, )) { if (touchedLockFile || - lockFileModified.isAfterSecond(packageConfigStat.modified)) { + lockFileModified.isAfter(packageConfigStat.modified)) { touch(packageConfigPath); } } else { @@ -1679,17 +1679,3 @@ See https://dart.dev/go/sdk-constraint /// For each package in a workspace, a set of changes to dependencies. typedef ChangeSet = Map>; - -extension on DateTime { - /// Whether this [DateTime] is strictly after [other] ignoring - /// sub-second precision. - /// - /// This is necessary because some filesystems or runtime APIs (like - /// manual `setLastModified` writes vs automatic OS writes) truncate - /// milliseconds to `.000`, which would otherwise incorrectly make - /// equal/newer files look older. - bool isAfterSecond(DateTime other) { - return millisecondsSinceEpoch ~/ 1000 > - other.millisecondsSinceEpoch ~/ 1000; - } -} diff --git a/lib/src/io.dart b/lib/src/io.dart index f23eac810e9..255dbbfc5be 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -1118,6 +1118,14 @@ class PubProcess { /// Updates [path]'s modification time. void touch(String path) { log.fine('Touching `$path`'); + if (Platform.isLinux || Platform.isMacOS) { + try { + Process.runSync('touch', [path]); + return; + } catch (_) { + // Fallback if touch command is somehow not found. + } + } File(path).setLastModifiedSync(DateTime.now()); } From f0dc3e4db1a287d78bbfd7301b3ab8f2ab6efa6e Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 28 May 2026 14:58:18 +0000 Subject: [PATCH 07/12] more specific catch --- lib/src/io.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/io.dart b/lib/src/io.dart index 255dbbfc5be..3b997b7ccc6 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -1122,7 +1122,7 @@ void touch(String path) { try { Process.runSync('touch', [path]); return; - } catch (_) { + } on Exception catch (_) { // Fallback if touch command is somehow not found. } } From 10c69507b50a249e019b6538ff10c152f89268ad Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Fri, 29 May 2026 13:08:22 +0000 Subject: [PATCH 08/12] Truncate timestamps on windows before comparison --- .../command/check_resolution_up_to_date.dart | 6 +- lib/src/entrypoint.dart | 63 ++++++++++++++----- lib/src/io.dart | 3 +- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/lib/src/command/check_resolution_up_to_date.dart b/lib/src/command/check_resolution_up_to_date.dart index 66fff46d7b2..99d7fb89281 100644 --- a/lib/src/command/check_resolution_up_to_date.dart +++ b/lib/src/command/check_resolution_up_to_date.dart @@ -34,7 +34,11 @@ Otherwise exit non-zero. @override Future runProtected() async { - final result = Entrypoint.isResolutionUpToDate(directory, cache); + final result = Entrypoint.isResolutionUpToDate( + directory, + cache, + updateOutOfDateTimestamps: false, + ); if (result == null) { fail('Resolution needs updating. Run `$topLevelProgram pub get`'); } else { diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index bc641902070..195f2572f03 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -826,9 +826,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without /// pubspec.lock. We do this extra round of checking to accommodate for cases /// where version control or other processes mess up the timestamp order. /// - /// If the resolution is still valid, the timestamps are updated and this - /// returns the package configuration and the root dir. Otherwise this - /// returns `null`. + /// If the resolution is still valid, the timestamps are updated (unless + /// [updateOutOfDateTimestamps] is false) and this returns the package + /// configuration and the root dir. Otherwise this returns `null`. /// /// This check is on the fast-path of `dart run` and should do as little /// work as possible. Specifically we avoid parsing any yaml when the @@ -845,8 +845,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without /// `.dart_tool/package_config.json` is not checked into version control. static (PackageConfig, String)? isResolutionUpToDate( String dir, - SystemCache cache, - ) { + SystemCache cache, { + bool updateOutOfDateTimestamps = true, + }) { late final wasRelative = p.isRelative(dir); String relativeIfNeeded(String path) => wasRelative ? p.relative(path) : path; @@ -1198,6 +1199,11 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without ); return null; } + final packageConfig = _loadPackageConfig(packageConfigPath); + if (p.isWithin(cache.rootDir, packageConfigPath)) { + // We always consider a global package (inside the cache) up-to-date. + return (packageConfig, rootDir); + } workspaceRoot = rootDir == rootPackageDir ? root @@ -1206,11 +1212,6 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without loadPubspec: Pubspec.loadRootWithSources(cache.sources), ); final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock')); - final packageConfig = _loadPackageConfig(packageConfigPath); - if (p.isWithin(cache.rootDir, packageConfigPath)) { - // We always consider a global package (inside the cache) up-to-date. - return (packageConfig, rootDir); - } /// Whether or not the `.dart_tool/package_config.json` file was /// generated by a different sdk down to changes in minor versions. @@ -1296,10 +1297,10 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without return null; } - if (!lockFileModified.isAfter(pubspecStat.modified)) { + if (!lockFileModified.isAfterWithPrecision(pubspecStat.modified)) { log.fine('`$pubspecPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecStat.modified.isAfter(lockFileModified)) { + if (pubspecStat.modified.isAfterWithPrecision(lockFileModified)) { pubspecStrictlyNewer = true; break; } @@ -1313,16 +1314,26 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without // This will wrongly require you to reresolve if a // `pubspec_overrides.yaml` in a path-dependency is updated. That // seems acceptable. - if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) { + if (!lockFileModified.isAfterWithPrecision( + pubspecOverridesStat.modified, + )) { log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); lockfileNewerThanPubspecs = false; - if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { + if (pubspecOverridesStat.modified.isAfterWithPrecision( + lockFileModified, + )) { pubspecStrictlyNewer = true; break; } } } } + if (!updateOutOfDateTimestamps && !lockfileNewerThanPubspecs) { + log.fine( + 'Timestamps are out of order (updateOutOfDateTimestamps: false)', + ); + return null; + } var touchedLockFile = false; late final lockFile = _loadLockFile(lockFilePath, cache); @@ -1337,9 +1348,17 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without } } + if (!updateOutOfDateTimestamps && + !packageConfigStat.modified.isAfterWithPrecision(lockFileModified)) { + log.fine( + 'Timestamps are out of order (updateOutOfDateTimestamps: false)', + ); + return null; + } + if (touchedLockFile || !lockfileNewerThanPubspecs || - !packageConfigStat.modified.isAfter(lockFileModified)) { + !packageConfigStat.modified.isAfterWithPrecision(lockFileModified)) { log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); if (isPackageConfigUpToDate( packageConfig, @@ -1349,7 +1368,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without lockFilePath: lockFilePath, )) { if (touchedLockFile || - lockFileModified.isAfter(packageConfigStat.modified)) { + lockFileModified.isAfterWithPrecision(packageConfigStat.modified)) { touch(packageConfigPath); } } else { @@ -1679,3 +1698,15 @@ See https://dart.dev/go/sdk-constraint /// For each package in a workspace, a set of changes to dependencies. typedef ChangeSet = Map>; + +extension on DateTime { + /// Whether this [DateTime] is strictly after [other], ignoring + /// sub-second precision on Windows due to `setLastModifiedSync` truncation. + bool isAfterWithPrecision(DateTime other) { + if (platform.isWindows) { + return millisecondsSinceEpoch ~/ 1000 > + other.millisecondsSinceEpoch ~/ 1000; + } + return isAfter(other); + } +} diff --git a/lib/src/io.dart b/lib/src/io.dart index 3b997b7ccc6..db81832aa3b 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -1118,8 +1118,9 @@ class PubProcess { /// Updates [path]'s modification time. void touch(String path) { log.fine('Touching `$path`'); - if (Platform.isLinux || Platform.isMacOS) { + if (platform.isLinux || platform.isMacOS) { try { + // setLastModifiedSync has poor resolution. We call out to touch instead. Process.runSync('touch', [path]); return; } on Exception catch (_) { From 7731a6fbf3386929ec10ab90079e91d9d1637603 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Fri, 29 May 2026 13:34:40 +0000 Subject: [PATCH 09/12] fix: resolve timestamp comparison issues on Windows and read-only check flakiness --- lib/src/entrypoint.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 195f2572f03..e53c9dad682 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -1349,7 +1349,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without } if (!updateOutOfDateTimestamps && - !packageConfigStat.modified.isAfterWithPrecision(lockFileModified)) { + packageConfigStat.modified.isBeforeWithPrecision(lockFileModified)) { log.fine( 'Timestamps are out of order (updateOutOfDateTimestamps: false)', ); @@ -1358,7 +1358,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without if (touchedLockFile || !lockfileNewerThanPubspecs || - !packageConfigStat.modified.isAfterWithPrecision(lockFileModified)) { + packageConfigStat.modified.isBeforeWithPrecision(lockFileModified)) { log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); if (isPackageConfigUpToDate( packageConfig, @@ -1709,4 +1709,14 @@ extension on DateTime { } return isAfter(other); } + + /// Whether this [DateTime] is strictly before [other], ignoring + /// sub-second precision on Windows due to `setLastModifiedSync` truncation. + bool isBeforeWithPrecision(DateTime other) { + if (platform.isWindows) { + return millisecondsSinceEpoch ~/ 1000 < + other.millisecondsSinceEpoch ~/ 1000; + } + return isBefore(other); + } } From ab5bbe4019d4c0d0caca40c0b9b6de0a463d8893 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Fri, 29 May 2026 13:35:33 +0000 Subject: [PATCH 10/12] fix: revert snapshot compilation check to avoid loop on Windows --- lib/src/executable.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 5638f5cbe9b..b0c2ccd4c6e 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -421,7 +421,7 @@ Future getExecutableForCommand( final packageConfigStat = tryStatFile(packageConfigPath); if (snapshotStat == null || packageConfigStat == null || - !snapshotStat.modified.isAfter(packageConfigStat.modified) || + packageConfigStat.modified.isAfter(snapshotStat.modified) || (await entrypoint.packageGraph).isPackageMutable(package)) { try { await errorsOnlyUnlessTerminal( From 11ccc981184686075e951a115e29052a447252ce Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Fri, 29 May 2026 13:58:00 +0000 Subject: [PATCH 11/12] fix: handle workspace packages in isLockFileUpToDate --- lib/src/entrypoint.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index e53c9dad682..1644ccd86f5 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -879,6 +879,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without /// Returns whether the locked version of [dep] matches the dependency. bool isDependencyUpToDate(PackageRange dep) { if (dep.name == root.name) return true; + if (workspaceRoot.transitiveWorkspace.any((p) => p.name == dep.name)) { + return true; + } final locked = lockFile.packages[dep.name]; return locked != null && dep.allows(locked); From b7ea680ecdaa7d116f0945e9738f8c28cabc779a Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Fri, 29 May 2026 14:00:29 +0000 Subject: [PATCH 12/12] docs: add comment explaining why workspace packages are always up-to-date in lockfile --- lib/src/entrypoint.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 1644ccd86f5..cc0fb6b6669 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -879,6 +879,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without /// Returns whether the locked version of [dep] matches the dependency. bool isDependencyUpToDate(PackageRange dep) { if (dep.name == root.name) return true; + // Workspace packages are local source packages and are never listed in + // the `packages` section of `pubspec.lock`. They are always considered + // up-to-date here. if (workspaceRoot.transitiveWorkspace.any((p) => p.name == dep.name)) { return true; }