Merge remote-tracking branch 'origin/cp_example' into merge-cp_example
diff --git a/.github/workflows/tag_from_sdk.yml b/.github/workflows/tag_from_sdk.yml new file mode 100644 index 0000000..6b684b8 --- /dev/null +++ b/.github/workflows/tag_from_sdk.yml
@@ -0,0 +1,22 @@ +name: Sync version tags from SDK +on: + schedule: + # Run every day at 00:00 + - cron: "0 0 * * *" + +jobs: + tag_from_sdk: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + with: + fetch-depth: 0 # Check out everything to be able to tag. + - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c + with: + sdk: stable + - id: install + name: Install dependencies + run: dart pub get + - id: tags + name: push_missing_tags_to_origin + run: dart tool/create_version_tags_from_sdk.dart --create --push
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3c302f3..1548ed9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml
@@ -24,7 +24,7 @@ matrix: sdk: [dev] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c with: sdk: ${{ matrix.sdk }} @@ -52,7 +52,7 @@ sdk: [dev] shard: [0, 1, 2, 3, 4, 5, 6] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c with: sdk: ${{ matrix.sdk }}
diff --git a/lib/pub.dart b/lib/pub.dart index d7b5653..12f364c 100644 --- a/lib/pub.dart +++ b/lib/pub.dart
@@ -8,6 +8,7 @@ import 'src/exceptions.dart'; import 'src/http.dart'; import 'src/pub_embeddable_command.dart'; +import 'src/source/git.dart'; import 'src/system_cache.dart'; export 'src/executable.dart' @@ -67,3 +68,39 @@ String message; ResolutionFailedException._(this.message); } + +/// Given a Git repo that contains a pub package, gets the name of the pub +/// package. +/// +/// Will download the repo to the system cache under the assumption that the +/// package will be downloaded afterwards. +/// +/// [url] points to the git repository. If it is a relative url, it is resolved +/// as a file url relative to the path [relativeTo]. +/// +/// [ref] is the commit, tag, or branch name where the package should be looked +/// up when fetching the name. If omitted, 'HEAD' is used. +/// +/// [tagPattern] is a string containing `'{{version}}'` as a substring, the +/// latest tag matching the pattern will be used for fetching the name. +/// +/// Only one of [ref] and [tagPattern] can be used. +/// +/// If [isOffline], only the already cached versions of the repo is used. +Future<String> getPackageNameFromGitRepo( + String url, { + String? ref, + String? path, + String? tagPattern, + String? relativeTo, + bool isOffline = false, +}) async { + return await GitSource.instance.getPackageNameFromRepo( + url, + ref, + path, + SystemCache(isOffline: isOffline), + relativeTo: relativeTo, + tagPattern: tagPattern, + ); +}
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index e6d6e54..23ba02b 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart
@@ -167,7 +167,6 @@ defaultsTo: true, help: 'Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).', - hide: true, ); }
diff --git a/lib/src/command/check_resolution_up_to_date.dart b/lib/src/command/check_resolution_up_to_date.dart new file mode 100644 index 0000000..66fff46 --- /dev/null +++ b/lib/src/command/check_resolution_up_to_date.dart
@@ -0,0 +1,44 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// 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:async'; + +import '../command.dart'; +import '../command_runner.dart'; +import '../entrypoint.dart'; +import '../log.dart' as log; +import '../utils.dart'; + +class CheckResolutionUpToDateCommand extends PubCommand { + @override + String get name => 'check-resolution-up-to-date'; + + @override + bool get hidden => true; + + @override + String get description => ''' +Do a fast timestamp-based check to see resolution is up-to-date and internally +consistent. + +If timestamps are correctly ordered, exit 0, and do not check the external sources for +newer versions. +Otherwise exit non-zero. +'''; + + @override + String get argumentsDescription => ''; + + CheckResolutionUpToDateCommand(); + + @override + Future<void> runProtected() async { + final result = Entrypoint.isResolutionUpToDate(directory, cache); + if (result == null) { + fail('Resolution needs updating. Run `$topLevelProgram pub get`'); + } else { + log.message('Resolution is up-to-date'); + } + } +}
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart index cd90376..1dccee6 100644 --- a/lib/src/command/downgrade.dart +++ b/lib/src/command/downgrade.dart
@@ -48,7 +48,6 @@ 'example', defaultsTo: true, help: 'Also run in `example/` (if it exists).', - hide: true, ); argParser.addOption(
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart index 978d6f6..9c08444 100644 --- a/lib/src/command/get.dart +++ b/lib/src/command/get.dart
@@ -56,7 +56,6 @@ 'example', defaultsTo: true, help: 'Also run in `example/` (if it exists).', - hide: true, ); argParser.addOption(
diff --git a/lib/src/command/login.dart b/lib/src/command/login.dart index 04e765b..0a505b6 100644 --- a/lib/src/command/login.dart +++ b/lib/src/command/login.dart
@@ -64,7 +64,9 @@ try { switch (json.decode(userInfoRequest.body)) { case {'name': final String? name, 'email': final String email}: - return _UserInfo(name, email); + return _UserInfo(name: name, email: email); + case {'email': final String email}: + return _UserInfo(name: null, email: email); default: log.fine( 'Bad response from $userInfoEndpoint: ${userInfoRequest.body}', @@ -84,7 +86,7 @@ class _UserInfo { final String? name; final String email; - _UserInfo(this.name, this.email); + _UserInfo({required this.name, required this.email}); @override String toString() => ['<$email>', name ?? ''].join(' '); }
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart index df648e9..5a68ca3 100644 --- a/lib/src/command/remove.dart +++ b/lib/src/command/remove.dart
@@ -59,7 +59,6 @@ 'example', defaultsTo: true, help: 'Also update dependencies in `example/` (if it exists).', - hide: true, ); argParser.addOption(
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 0c25686..7078ac0 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -89,7 +89,6 @@ 'example', defaultsTo: true, help: 'Also run in `example/` (if it exists).', - hide: true, ); argParser.addOption(
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 9ced67e..9a372f1 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart
@@ -13,6 +13,7 @@ import 'command/add.dart'; import 'command/bump.dart'; import 'command/cache.dart'; +import 'command/check_resolution_up_to_date.dart'; import 'command/deps.dart'; import 'command/downgrade.dart'; import 'command/get.dart'; @@ -151,6 +152,7 @@ addCommand(OutdatedCommand()); addCommand(RemoveCommand()); addCommand(RunCommand()); + addCommand(CheckResolutionUpToDateCommand()); addCommand(UpgradeCommand()); addCommand(UnpackCommand()); addCommand(UploaderCommand());
diff --git a/lib/src/dart.dart b/lib/src/dart.dart index 07d50eb..8592781 100644 --- a/lib/src/dart.dart +++ b/lib/src/dart.dart
@@ -47,8 +47,8 @@ path = p.normalize(p.absolute(path)); final parseResult = _session.getParsedUnit(path); if (parseResult is ParsedUnitResult) { - if (parseResult.errors.isNotEmpty) { - throw AnalyzerErrorGroup(parseResult.errors); + if (parseResult.diagnostics.isNotEmpty) { + throw AnalyzerErrorGroup(parseResult.diagnostics); } return parseResult.unit; } else {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 2eda882..62dfa78 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -792,23 +792,47 @@ } } - /// Does a fast-pass check to see if the resolution is up-to-date. If not, run - /// a resolution with `pub get` semantics. + /// The [PackageConfig] object representing `.dart_tool/package_config.json` + /// along with the dir where it resides, if it and `pubspec.lock` exist and + /// are up to date with respect to pubspec.yaml and its dependencies. Or + /// `null` if it is outdated. /// - /// If [summaryOnly] is `true` (the default) only a short summary is shown of - /// the solve. + /// Always returns `null` if `.dart_tool/package_config.json` was generated + /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a + /// different Dart or Flutter SDK version. /// - /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no - /// output if no terminal is attached. + /// Otherwise first the `modified` timestamps are compared, and if + /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is + /// newer than all pubspec.yamls of all packages in + /// `.dart_tool/package_config.json` we short-circuit and return true. /// - /// When succesfull returns the found/created `PackageConfig` and the - /// directory containing it. - static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate( - String dir, { - required SystemCache cache, - bool summaryOnly = true, - bool onlyOutputWhenTerminal = true, - }) async { + /// If any of the timestamps are out of order, the resolution in + /// pubspec.lock is validated against constraints of all pubspec.yamls, and + /// the packages of `.dart_tool/package_config.json` is validated against + /// 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`. + /// + /// 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 + /// timestamps are in the right order. + /// + /// `.dart_tool/package_config.json` is read and parsed. In the case of `dart + /// run` this is acceptable: we speculate that it brings it to the file + /// system cache and the dart VM is going to read the file anyways. + /// + /// Note this procedure will give false positives if the timestamps are + /// artificially brought in the "right" order. (eg. by manually running + /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is + /// hard to avoid, but also unlikely to happen by accident because + /// `.dart_tool/package_config.json` is not checked into version control. + static (PackageConfig, String)? isResolutionUpToDate( + String dir, + SystemCache cache, + ) { late final wasRelative = p.isRelative(dir); String relativeIfNeeded(String path) => wasRelative ? p.relative(path) : path; @@ -1044,271 +1068,254 @@ return true; } - /// The [PackageConfig] object representing `.dart_tool/package_config.json` - /// along with the dir where it resides, if it and `pubspec.lock` exist and - /// are up to date with respect to pubspec.yaml and its dependencies. Or - /// `null` if it is outdated. - /// - /// Always returns `null` if `.dart_tool/package_config.json` was generated - /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a - /// different Dart or Flutter SDK version. - /// - /// Otherwise first the `modified` timestamps are compared, and if - /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is - /// newer than all pubspec.yamls of all packages in - /// `.dart_tool/package_config.json` we short-circuit and return true. - /// - /// If any of the timestamps are out of order, the resolution in - /// pubspec.lock is validated against constraints of all pubspec.yamls, and - /// the packages of `.dart_tool/package_config.json` is validated against - /// pubspec.lock. We do this extra round of checking to accomodate 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`. - /// - /// 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 - /// timestamps are in the right order. - /// - /// `.dart_tool/package_config.json` is read parsed. In the case of `dart - /// run` this is acceptable: we speculate that it brings it to the file - /// system cache and the dart VM is going to read the file anyways. - /// - /// Note this procedure will give false positives if the timestamps are - /// artificially brought in the "right" order. (eg. by manually running - /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is - /// hard to avoid, but also unlikely to happen by accident because - /// `.dart_tool/package_config.json` is not checked into version control. - (PackageConfig, String)? isResolutionUpToDate() { - FileStat? packageConfigStat; - late final String packageConfigPath; - late final String rootDir; - for (final parent in parentDirs(dir)) { - final potentialPackageConfigPath = p.normalize( - p.join(parent, '.dart_tool', 'package_config.json'), + FileStat? packageConfigStat; + late final String packageConfigPath; + late final String rootDir; + for (final parent in parentDirs(dir)) { + final potentialPackageConfigPath = p.normalize( + p.join(parent, '.dart_tool', 'package_config.json'), + ); + packageConfigStat = tryStatFile(potentialPackageConfigPath); + + if (packageConfigStat != null) { + packageConfigPath = potentialPackageConfigPath; + rootDir = parent; + break; + } + final potentialPubspecPath = p.join(parent, 'pubspec.yaml'); + if (tryStatFile(potentialPubspecPath) == null) { + // No package at [parent] continue to next dir. + continue; + } + + final potentialWorkspaceRefPath = p.normalize( + p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'), + ); + + final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath); + if (workspaceRefText == null) { + log.fine( + '`$potentialPubspecPath` exists without corresponding ' + '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.', ); - packageConfigStat = tryStatFile(potentialPackageConfigPath); - - if (packageConfigStat != null) { - packageConfigPath = potentialPackageConfigPath; - rootDir = parent; - break; - } - final potentialPubspecPath = p.join(parent, 'pubspec.yaml'); - if (tryStatFile(potentialPubspecPath) == null) { - // No package at [parent] continue to next dir. - continue; - } - - final potentialWorkspaceRefPath = p.normalize( - p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'), - ); - - final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath); - if (workspaceRefText == null) { - log.fine( - '`$potentialPubspecPath` exists without corresponding ' - '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.', - ); - return null; - } else { - try { - if (jsonDecode(workspaceRefText) case { - 'workspaceRoot': final String path, - }) { - final potentialPackageConfigPath2 = relativeIfNeeded( + return null; + } else { + try { + if (jsonDecode(workspaceRefText) case { + 'workspaceRoot': final String path, + }) { + final potentialPackageConfigPath2 = relativeIfNeeded( + p.normalize( + p.absolute( + p.join( + p.dirname(potentialWorkspaceRefPath), + path, + '.dart_tool', + 'package_config.json', + ), + ), + ), + ); + packageConfigStat = tryStatFile(potentialPackageConfigPath2); + if (packageConfigStat == null) { + log.fine( + '`$potentialWorkspaceRefPath` points to non-existing ' + '`$potentialPackageConfigPath2`', + ); + return null; + } else { + packageConfigPath = potentialPackageConfigPath2; + rootDir = relativeIfNeeded( p.normalize( p.absolute( - p.join( - p.dirname(potentialWorkspaceRefPath), - path, - '.dart_tool', - 'package_config.json', - ), + p.join(p.dirname(potentialWorkspaceRefPath), path), ), ), ); - packageConfigStat = tryStatFile(potentialPackageConfigPath2); - if (packageConfigStat == null) { - log.fine( - '`$potentialWorkspaceRefPath` points to non-existing ' - '`$potentialPackageConfigPath2`', - ); - return null; - } else { - packageConfigPath = potentialPackageConfigPath2; - rootDir = relativeIfNeeded( - p.normalize( - p.absolute( - p.join(p.dirname(potentialWorkspaceRefPath), path), - ), - ), - ); - break; - } - } else { - log.fine( - '`$potentialWorkspaceRefPath` ' - 'is missing "workspaceRoot" property', - ); - return null; + break; } - } on FormatException catch (e) { - log.fine('`$potentialWorkspaceRefPath` not valid json: $e.'); + } else { + log.fine( + '`$potentialWorkspaceRefPath` ' + 'is missing "workspaceRoot" property', + ); return null; } - } - } - if (packageConfigStat == null) { - log.fine( - 'Found no .dart_tool/package_config.json - no existing resolution.', - ); - return null; - } - 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 is was - /// generated by a different sdk down to changes in minor versions. - bool isPackageConfigGeneratedBySameDartSdk() { - final generatorVersion = packageConfig.generatorVersion; - if (generatorVersion == null || - generatorVersion.major != sdk.version.major || - generatorVersion.minor != sdk.version.minor) { - log.fine('The Dart SDK was updated since last package resolution.'); - return false; - } - return true; - } - - final flutter = FlutterSdk(); - // If Flutter has moved since last invocation, we want to have new - // sdk-packages, and therefore do a new resolution. - // - // This also counts if Flutter was introduced or removed. - final flutterRoot = - flutter.rootDirectory == null - ? null - : p.toUri(p.absolute(flutter.rootDirectory!)).toString(); - if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) { - log.fine('Flutter has moved since last invocation.'); - return null; - } - if (packageConfig.additionalProperties['flutterVersion'] != - (flutter.isAvailable ? flutter.version.toString() : null)) { - log.fine('Flutter has updated since last invocation.'); - return null; - } - // If the pub cache was moved we should have a new resolution. - final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString(); - if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) { - final previousPubCachePath = - packageConfig.additionalProperties['pubCache']; - log.fine( - 'The pub cache has moved from $previousPubCachePath to $rootCacheUrl ' - 'since last invocation.', - ); - return null; - } - // If the Dart sdk was updated we want a new resolution. - if (!isPackageConfigGeneratedBySameDartSdk()) { - return null; - } - final lockFileStat = tryStatFile(lockFilePath); - if (lockFileStat == null) { - log.fine('No $lockFilePath file found.'); - return null; - } - - final lockFileModified = lockFileStat.modified; - var lockfileNewerThanPubspecs = true; - - // Check that all packages in packageConfig exist and their pubspecs have - // not been updated since the lockfile was written. - for (var package in packageConfig.packages) { - final pubspecPath = p.normalize( - p.join( - rootDir, - '.dart_tool', - package.rootUri - // Important to use `toFilePath()` here rather than `path`, as - // it handles Url-decoding. - .toFilePath(), - '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. + } on FormatException catch (e) { + log.fine('`$potentialWorkspaceRefPath` not valid json: $e.'); return null; } - - if (pubspecStat.modified.isAfter(lockFileModified)) { - log.fine('`$pubspecPath` is newer than `$lockFilePath`'); - lockfileNewerThanPubspecs = false; - break; - } - final pubspecOverridesPath = p.join( - package.rootUri.path, - 'pubspec_overrides.yaml', - ); - final pubspecOverridesStat = tryStatFile(pubspecOverridesPath); - if (pubspecOverridesStat != null) { - // 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)) { - log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); - lockfileNewerThanPubspecs = false; - } - } } - var touchedLockFile = false; - late final lockFile = _loadLockFile(lockFilePath, cache); - late final root = Package.load( - dir, - loadPubspec: Pubspec.loadRootWithSources(cache.sources), + } + if (packageConfigStat == null) { + log.fine( + 'Found no .dart_tool/package_config.json - no existing resolution.', ); - - if (!lockfileNewerThanPubspecs) { - if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) { - touch(lockFilePath); - touchedLockFile = true; - } else { - return null; - } - } - - if (touchedLockFile || - lockFileModified.isAfter(packageConfigStat.modified)) { - log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); - if (isPackageConfigUpToDate( - packageConfig, - lockFile, - root, - packageConfigPath: packageConfigPath, - lockFilePath: lockFilePath, - )) { - touch(packageConfigPath); - } else { - return null; - } - } + return null; + } + 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); } - if (isResolutionUpToDate() case ( + /// Whether or not the `.dart_tool/package_config.json` file was + /// generated by a different sdk down to changes in minor versions. + bool isPackageConfigGeneratedBySameDartSdk() { + final generatorVersion = packageConfig.generatorVersion; + if (generatorVersion == null || + generatorVersion.major != sdk.version.major || + generatorVersion.minor != sdk.version.minor) { + log.fine('The Dart SDK was updated since last package resolution.'); + return false; + } + return true; + } + + final flutter = FlutterSdk(); + // If Flutter has moved since last invocation, we want to have new + // sdk-packages, and therefore do a new resolution. + // + // This also counts if Flutter was introduced or removed. + final flutterRoot = + flutter.rootDirectory == null + ? null + : p.toUri(p.absolute(flutter.rootDirectory!)).toString(); + if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) { + log.fine('Flutter has moved since last invocation.'); + return null; + } + if (packageConfig.additionalProperties['flutterVersion'] != + (flutter.isAvailable ? flutter.version.toString() : null)) { + log.fine('Flutter has updated since last invocation.'); + return null; + } + // If the pub cache was moved we should have a new resolution. + final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString(); + if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) { + final previousPubCachePath = + packageConfig.additionalProperties['pubCache']; + log.fine( + 'The pub cache has moved from $previousPubCachePath to $rootCacheUrl ' + 'since last invocation.', + ); + return null; + } + // If the Dart sdk was updated we want a new resolution. + if (!isPackageConfigGeneratedBySameDartSdk()) { + return null; + } + final lockFileStat = tryStatFile(lockFilePath); + if (lockFileStat == null) { + log.fine('No $lockFilePath file found.'); + return null; + } + + final lockFileModified = lockFileStat.modified; + var lockfileNewerThanPubspecs = true; + + // Check that all packages in packageConfig exist and their pubspecs have + // not been updated since the lockfile was written. + for (var package in packageConfig.packages) { + final pubspecPath = p.normalize( + p.join( + rootDir, + '.dart_tool', + package.rootUri + // Important to use `toFilePath()` here rather than `path`, as + // it handles Url-decoding. + .toFilePath(), + '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 (pubspecStat.modified.isAfter(lockFileModified)) { + log.fine('`$pubspecPath` is newer than `$lockFilePath`'); + lockfileNewerThanPubspecs = false; + break; + } + final pubspecOverridesPath = p.join( + package.rootUri.path, + 'pubspec_overrides.yaml', + ); + final pubspecOverridesStat = tryStatFile(pubspecOverridesPath); + if (pubspecOverridesStat != null) { + // 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)) { + log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); + lockfileNewerThanPubspecs = false; + } + } + } + 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)) { + touch(lockFilePath); + touchedLockFile = true; + } else { + return null; + } + } + + if (touchedLockFile || + lockFileModified.isAfter(packageConfigStat.modified)) { + log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); + if (isPackageConfigUpToDate( + packageConfig, + lockFile, + root, + packageConfigPath: packageConfigPath, + lockFilePath: lockFilePath, + )) { + touch(packageConfigPath); + } else { + return null; + } + } + return (packageConfig, rootDir); + } + + /// Does a fast-pass check to see if the resolution is up-to-date. If not, run + /// a resolution with `pub get` semantics. + /// + /// If [summaryOnly] is `true` (the default) only a short summary is shown of + /// the solve. + /// + /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no + /// output if no terminal is attached. + /// + /// When succesfull returns the found/created `PackageConfig` and the + /// directory containing it. + static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate( + String dir, { + required SystemCache cache, + bool summaryOnly = true, + bool onlyOutputWhenTerminal = true, + }) async { + late final wasRelative = p.isRelative(dir); + String relativeIfNeeded(String path) => + wasRelative ? p.relative(path) : path; + + if (isResolutionUpToDate(dir, cache) case ( final PackageConfig packageConfig, final String rootDir, )) {
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart index 408d00a..0ed7723 100644 --- a/lib/src/pub_embeddable_command.dart +++ b/lib/src/pub_embeddable_command.dart
@@ -7,6 +7,7 @@ import 'command/add.dart'; import 'command/bump.dart'; import 'command/cache.dart'; +import 'command/check_resolution_up_to_date.dart'; import 'command/deps.dart'; import 'command/downgrade.dart'; import 'command/get.dart'; @@ -83,6 +84,7 @@ addSubcommand(LishCommand()); addSubcommand(OutdatedCommand()); addSubcommand(RemoveCommand()); + addSubcommand(CheckResolutionUpToDateCommand()); addSubcommand(RunCommand(deprecated: true, alwaysUseSubprocess: true)); addSubcommand(UnpackCommand()); addSubcommand(UpgradeCommand());
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart index 7f03059..9087944 100644 --- a/lib/src/source/git.dart +++ b/lib/src/source/git.dart
@@ -290,7 +290,7 @@ String? ref, String? path, SystemCache cache, { - required String relativeTo, + required String? relativeTo, required String? tagPattern, }) async { assert(
diff --git a/pubspec.lock b/pubspec.lock index 5bae051..c4db465 100644 --- a/pubspec.lock +++ b/pubspec.lock
@@ -5,18 +5,18 @@ dependency: transitive description: name: _fe_analyzer_shared - sha256: c81659312e021e3b780a502206130ea106487b34793bce61e26dc0f9b84807af + sha256: dd3d2ad434b9510001d089e8de7556d50c834481b9abc2891a0184a8493a19dc url: "https://pub.dev" source: hosted - version: "83.0.0" + version: "89.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "9c35a79bf2a150b3ea0d40010fbbb45b5ebea143d47096e0f82fd922a324b49b" + sha256: c22b6e7726d1f9e5db58c7251606076a71ca0dbcf76116675edfadbec0c9e875 url: "https://pub.dev" source: hosted - version: "7.4.6" + version: "8.2.0" args: dependency: "direct main" description: @@ -45,10 +45,10 @@ dependency: "direct dev" description: name: checks - sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9 + sha256: "016871c84732c1ac9856b8940236d5a5802ba638b3bd3e0ea7027b51a35f7aa7" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" cli_config: dependency: transitive description: @@ -85,10 +85,10 @@ dependency: transitive description: name: coverage - sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.14.1" + version: "1.15.0" crypto: dependency: "direct main" description: @@ -141,10 +141,10 @@ dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_multi_server: dependency: "direct main" description: @@ -365,26 +365,26 @@ dependency: "direct dev" description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" test_descriptor: dependency: "direct dev" description: @@ -421,10 +421,10 @@ dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" web: dependency: transitive description: @@ -474,4 +474,4 @@ source: hosted version: "2.2.2" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml index 808dda2..38570c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -4,7 +4,7 @@ sdk: ^3.7.0 dependencies: - analyzer: ^7.4.5 + analyzer: ^8.2.0 args: ^2.7.0 async: ^2.13.0 cli_util: ^0.4.2 @@ -13,7 +13,7 @@ crypto: ^3.0.6 frontend_server_client: ^4.0.0 graphs: ^2.3.2 - http: ^1.4.0 + http: ^1.5.0 http_multi_server: ^3.2.2 http_parser: ^4.1.2 meta: ^1.17.0 @@ -29,9 +29,9 @@ yaml_edit: ^2.2.2 dev_dependencies: - checks: ^0.3.0 + checks: ^0.3.1 dart_flutter_team_lints: ^3.5.2 shelf_test_handler: ^2.0.2 - test: ^1.26.2 + test: ^1.26.3 test_descriptor: ^2.0.2 test_process: ^2.1.1
diff --git a/test/check_resolution_up_to_date_test.dart b/test/check_resolution_up_to_date_test.dart new file mode 100644 index 0000000..f9a6f33 --- /dev/null +++ b/test/check_resolution_up_to_date_test.dart
@@ -0,0 +1,112 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// 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 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +void main() { + test('Will exit non-zero if there are changes', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + + await d.appDir(dependencies: {'foo': '1.0.0'}).create(); + + await runPub( + args: ['check-resolution-up-to-date'], + error: contains('Resolution needs updating. Run `dart pub get`'), + + exitCode: 1, + ); + + await d.dir(appPath, [ + d.nothing('pubspec.lock'), + d.nothing('.dart_tool/package_config.json'), + ]).validate(); + + await pubGet(); + + await runPub( + args: ['check-resolution-up-to-date'], + output: contains('Resolution is up-to-date'), + exitCode: 0, + ); + + // Timestamp resolution is rather poor especially on windows. + await Future<Null>.delayed(const Duration(seconds: 1)); + + await d.appDir(dependencies: {'foo': '2.0.0'}).create(); + + await runPub( + args: ['check-resolution-up-to-date'], + error: contains('Resolution needs updating. Run `dart pub get`'), + exitCode: 1, + ); + }); + + test('Works in a workspace', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myapp', + '1.0.0', + sdk: '^3.5.0', + deps: {'foo': '1.0.0'}, + extras: { + 'workspace': ['pkg'], + }, + ), + d.dir('pkg', [d.libPubspec('pkg', '1.0.0', resolutionWorkspace: true)]), + ]).create(); + + await runPub( + args: ['check-resolution-up-to-date'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + workingDirectory: p.join(d.sandbox, appPath, 'pkg'), + error: contains('Resolution needs updating. Run `dart pub get`'), + exitCode: 1, + ); + + await pubGet( + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + workingDirectory: p.join(d.sandbox, appPath, 'pkg'), + output: contains('+ foo 1.0.0'), + ); + + await runPub( + args: ['check-resolution-up-to-date'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + workingDirectory: p.join(d.sandbox, appPath, 'pkg'), + output: contains('Resolution is up-to-date'), + exitCode: 0, + ); + + // Timestamp resolution is rather poor especially on windows. + await Future<Null>.delayed(const Duration(seconds: 1)); + + await d.dir(appPath, [ + d.libPubspec( + 'myapp', + '1.0.0', + sdk: '^3.5.0', + deps: {'foo': '1.0.0'}, + extras: { + 'workspace': ['pkg'], + }, + ), + ]).create(); + + await runPub( + args: ['check-resolution-up-to-date'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + workingDirectory: p.join(d.sandbox, appPath, 'pkg'), + error: contains('Resolution needs updating. Run `dart pub get`'), + exitCode: 1, + ); + }); +}
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt index f6f73fb..4aca35f 100644 --- a/test/testdata/goldens/help_test/pub add --help.txt +++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -43,6 +43,8 @@ -n, --dry-run Report what dependencies would change but don't change any. --[no-]precompile Build executables in immediate dependencies. -C, --directory=<dir> Run this in the directory <dir>. + --[no-]example Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists). + (defaults to on) Run "pub help" to see global options. See https://dart.dev/tools/pub/cmd/pub-add for detailed documentation.
diff --git a/test/testdata/goldens/help_test/pub downgrade --help.txt b/test/testdata/goldens/help_test/pub downgrade --help.txt index a971395..0b86d3e 100644 --- a/test/testdata/goldens/help_test/pub downgrade --help.txt +++ b/test/testdata/goldens/help_test/pub downgrade --help.txt
@@ -10,6 +10,8 @@ -h, --help Print this usage information. --[no-]offline Use cached packages instead of accessing the network. -n, --dry-run Report what dependencies would change but don't change any. + --[no-]example Also run in `example/` (if it exists). + (defaults to on) -C, --directory=<dir> Run this in the directory <dir>. --tighten Updates lower bounds in pubspec.yaml to match the resolved version.
diff --git a/test/testdata/goldens/help_test/pub get --help.txt b/test/testdata/goldens/help_test/pub get --help.txt index dd5aaf5..3c68ab1 100644 --- a/test/testdata/goldens/help_test/pub get --help.txt +++ b/test/testdata/goldens/help_test/pub get --help.txt
@@ -12,6 +12,8 @@ has changed. Useful for CI or deploying to production. --[no-]precompile Build executables in immediate dependencies. + --[no-]example Also run in `example/` (if it exists). + (defaults to on) -C, --directory=<dir> Run this in the directory <dir>. Run "pub help" to see global options.
diff --git a/test/testdata/goldens/help_test/pub remove --help.txt b/test/testdata/goldens/help_test/pub remove --help.txt index 2032abf..34e1463 100644 --- a/test/testdata/goldens/help_test/pub remove --help.txt +++ b/test/testdata/goldens/help_test/pub remove --help.txt
@@ -16,6 +16,8 @@ --[no-]offline Use cached packages instead of accessing the network. -n, --dry-run Report what dependencies would change but don't change any. --[no-]precompile Precompile executables in immediate dependencies. + --[no-]example Also update dependencies in `example/` (if it exists). + (defaults to on) -C, --directory=<dir> Run this in the directory <dir>. Run "pub help" to see global options.
diff --git a/test/testdata/goldens/help_test/pub upgrade --help.txt b/test/testdata/goldens/help_test/pub upgrade --help.txt index ade5a8d..90030c7 100644 --- a/test/testdata/goldens/help_test/pub upgrade --help.txt +++ b/test/testdata/goldens/help_test/pub upgrade --help.txt
@@ -12,6 +12,8 @@ --tighten Updates lower bounds in pubspec.yaml to match the resolved version. --unlock-transitive Also upgrades the transitive dependencies of the listed [dependencies] --major-versions Upgrades packages to their latest resolvable versions, and updates pubspec.yaml. + --[no-]example Also run in `example/` (if it exists). + (defaults to on) -C, --directory=<dir> Run this in the directory <dir>. Run "pub help" to see global options.
diff --git a/tool/create_version_tags_from_sdk.dart b/tool/create_version_tags_from_sdk.dart new file mode 100644 index 0000000..3c67f33 --- /dev/null +++ b/tool/create_version_tags_from_sdk.dart
@@ -0,0 +1,196 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a + +/// Tool for finding all tagged versions of the dart sdk, and in turn tag this +/// repository with `SDK-$version` for each of these. +library; + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; + +Future<void> main(List<String> args) async { + final argParser = + ArgParser() + ..addFlag( + 'create', + help: 'Create the missing tags, otherwise only list them', + negatable: false, + ) + ..addFlag( + 'push', + help: 'Push missing sdk tags to remote', + negatable: false, + ) + ..addOption('sdk-dir'); + final ArgResults argResults; + try { + argResults = argParser.parse(args); + } on FormatException catch (e) { + stderr.writeln('${e.message}\n${argParser.usage}'); + stderr.writeln(''' +Will find all tagged sdk versions, and tag the corresponding revision in this +repository with `--create`. +And can push these tags to the remote with `--push`. + +Usage: create_version_tags_from_sdk [--create] [--push] [--sdk-dir <path>] +'''); + exit(-1); + } + final create = argResults.flag('create'); + final push = argResults.flag('push'); + + Directory? tempDir; + String sdkDir; + try { + if (argResults.option('sdk-dir') == null) { + tempDir = Directory.systemTemp.createTempSync(); + final cloneResult = Process.runSync('git', [ + 'clone', + // Using a treeless clone is faster up-front, but slower for + // showing a specific revision. + // We assume we only miss a few tags. + '--filter=tree:0', + '-n', + 'https://github.com/dart-lang/sdk', + ], workingDirectory: tempDir.path); + if (cloneResult.exitCode != 0) { + throw Exception( + 'Failed to clone sdk ${cloneResult.stderr} ${cloneResult.stdout}', + ); + } + sdkDir = p.join(tempDir.path, 'sdk'); + } else { + sdkDir = argResults.option('sdk-dir')!; + } + + final sdkTags = + (Process.runSync('git', [ + 'ls-remote', + '--tags', + '--refs', + 'origin', + ], workingDirectory: sdkDir).stdout + as String) + .split('\n') + .where((line) => line.isNotEmpty) + .map((line) => line.split('\t')[1]) + .map((x) => x.substring('refs/tags/'.length)) + .where((x) { + try { + Version.parse(x); + } on FormatException { + return false; + } + return true; + }) + .toSet(); + final alreadyTagged = + (Process.runSync('git', [ + 'ls-remote', + '--tags', + '--refs', + 'origin', + ], workingDirectory: Directory.current.path).stdout + as String) + .split('\n') + .where((line) => line.isNotEmpty) + .map((line) => line.split('\t')[1]) + .map((x) => x.substring('refs/tags/'.length)) + .where((x) => x.startsWith('SDK-')) + .map((x) => x.substring('SDK-'.length)) + .toSet(); + final missing = sdkTags.difference(alreadyTagged); + var createdTagCount = 0; + var pushedTagCount = 0; + + if (missing.isNotEmpty) { + for (final sdkTag in missing) { + final version = Version.parse(sdkTag); + if ( + // Old versions of the sdk had no pub or no DEPS file. + version <= (Version.parse('1.11.3')) || + // These version seems to have a broken DEPS file. + version == Version.parse('1.12.0-dev.5.6') || + version == Version.parse('1.12.0-dev.5.7')) { + continue; + } + final depsResult = Process.runSync('git', [ + 'show', + '$sdkTag:DEPS', + ], workingDirectory: sdkDir); + if (depsResult.exitCode != 0) { + stderr.writeln( + 'Failed to get deps for $sdkTag ${depsResult.stderr} ' + '${depsResult.stdout}', + ); + continue; + } + + // Could use `gclient getdep -r sdk/third_party/pkg/pub` instead of a + // regexp. But for some versions that seems to not work well. + // The regexp + var pubRev = RegExp( + '"pub_rev": "([^"]*)"', + ).firstMatch(depsResult.stdout as String)?.group(1); + if (pubRev == null || pubRev.isEmpty) { + stderr.writeln('Failed to get pub rev for $sdkTag '); + continue; + } + if (pubRev.startsWith('@')) { + pubRev = pubRev.substring(1); + } + + stdout.writeln('$sdkTag uses pub: $pubRev'); + if (create) { + final tagResult = Process.runSync('git', [ + '-c', 'user.email=support@pub.dev', // + '-c', 'user.name=Pub tagging bot', // + 'tag', + 'SDK-$sdkTag', + pubRev, + '--annotate', + '--force', + '--message', 'SDK $sdkTag', // + ], workingDirectory: Directory.current.path); + if (tagResult.exitCode != 0) { + stderr.writeln( + 'Failed to tag sdk ${tagResult.stderr} ${tagResult.stdout}', + ); + continue; + } + createdTagCount++; + } + if (push) { + final pushResult = Process.runSync('git', [ + 'push', + 'origin', + // Don't run any hooks before pushing. + '--no-verify', + 'tag', + 'SDK-$sdkTag', + ], workingDirectory: Directory.current.path); + if (pushResult.exitCode != 0) { + stderr.writeln( + 'Failed to push sdk ${pushResult.stderr} ${pushResult.stdout}', + ); + continue; + } + pushedTagCount++; + } + } + } + if (!create) { + stdout.writeln('Would have created $createdTagCount tags'); + } else { + stdout.writeln('Created $createdTagCount tags'); + } + if (push) { + stdout.writeln('Pushed $pushedTagCount tags'); + } + } finally { + tempDir?.deleteSync(recursive: true); + } +}