diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 7078ac01014..8f12391ac35 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart @@ -17,6 +17,7 @@ import '../package_name.dart'; import '../pubspec.dart'; import '../pubspec_utils.dart'; import '../solver.dart'; +import '../solver/version_solver.dart'; import '../utils.dart'; /// Handles the `upgrade` pub command. @@ -25,9 +26,16 @@ class UpgradeCommand extends PubCommand { String get name => 'upgrade'; @override String get description => - "Upgrade the current package's dependencies to latest versions."; + "Upgrade the current package's dependencies to latest versions.\n" + '\n' + 'Append `:latest` to a dependency to require the latest available ' + 'version.\n' + '\n' + 'Append `:resolvable` to require the newest version resolvable with the ' + 'rest of\n' + 'the dependency graph.'; @override - String get argumentsDescription => '[dependencies...]'; + String get argumentsDescription => '[dependencies[:latest|:resolvable]...]'; @override String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-upgrade'; @@ -111,6 +119,15 @@ class UpgradeCommand extends PubCommand { late final Future> _packagesToUpgrade = _computePackagesToUpgrade(); + late final List<_UpgradeTarget> _upgradeTargets = + argResults.rest.map(_parseUpgradeTarget).toList(); + + late final Future?> _additionalConstraints = + _upgradeTargetConstraints(); + + late final Future> _latestResolvablePackages = + _computeLatestResolvablePackages(); + /// List of package names to upgrade, if empty then upgrade all packages. /// /// This allows the user to specify list of names that they want the @@ -118,11 +135,11 @@ class UpgradeCommand extends PubCommand { Future> _computePackagesToUpgrade() async { if (argResults.flag('unlock-transitive')) { final graph = await entrypoint.packageGraph; - return argResults.rest + return _upgradeTargets .expand( - (package) => graph + (target) => graph .transitiveDependencies( - package, + target.name, followDevDependenciesFromPackage: true, ) .map((p) => p.name), @@ -130,7 +147,7 @@ class UpgradeCommand extends PubCommand { .toSet() .toList(); } else { - return argResults.rest; + return _upgradeTargets.map((target) => target.name).toList(); } } @@ -152,6 +169,18 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); ), ); } + if (_upgradeTargets.any((target) => target.kind != null)) { + if (_upgradeMajorVersions) { + usageException( + 'Cannot use `:latest` or `:resolvable` with `--major-versions`.', + ); + } + if (_tighten) { + usageException( + 'Cannot use `:latest` or `:resolvable` with `--tighten`.', + ); + } + } if (_upgradeMajorVersions) { if (argResults.flag('example')) { @@ -196,6 +225,7 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); await e.acquireDependencies( SolveType.upgrade, unlock: await _packagesToUpgrade, + additionalConstraints: await _additionalConstraints, dryRun: _dryRun, precompile: _precompile, summaryOnly: onlySummary, @@ -204,6 +234,118 @@ Consider using the Dart 2.19 sdk to migrate to null safety.'''); _showOfflineWarning(); } + Future?> _upgradeTargetConstraints() async { + final constraintFutures = >[]; + for (final target in _upgradeTargets) { + final kind = target.kind; + if (kind == null) continue; + + constraintFutures.add( + (() async { + final targetPackage = switch (kind) { + _UpgradeTargetKind.latest => await _latest(target.name), + _UpgradeTargetKind.resolvable => await _latestResolvable( + target.name, + ), + }; + return ConstraintAndCause( + targetPackage.toRange(), + '${targetPackage.name} ${targetPackage.version} was requested by ' + '`$topLevelProgram pub upgrade ${target.argument}`.', + ); + })(), + ); + } + final constraints = await Future.wait(constraintFutures); + return constraints.isEmpty ? null : constraints; + } + + Future> _computeLatestResolvablePackages() async { + final solveResult = await log.spinner('Resolving dependencies', () async { + return await resolveVersions( + SolveType.upgrade, + cache, + entrypoint.workspaceRoot.transformWorkspace( + (package) => stripVersionBounds(package.pubspec), + ), + ); + }, condition: _shouldShowSpinner); + return {for (final package in solveResult.packages) package.name: package}; + } + + Future _latestResolvable(String package) async { + if (_packageRef(package) == null) { + dataError('Package `$package` is not in the current resolution.'); + } + final latestResolvable = (await _latestResolvablePackages)[package]; + if (latestResolvable == null) { + dataError( + 'Package `$package` is not in the latest resolvable resolution.', + ); + } + return latestResolvable; + } + + Future _latest(String package) async { + final ref = _packageRef(package); + if (ref == null) { + dataError('Package `$package` is not in the current resolution.'); + } + final current = entrypoint.lockFile.packages[package]; + final latest = await cache.getLatest(ref, version: current?.version); + if (latest == null) { + dataError('Could not find package `$package`.'); + } + return latest; + } + + PackageRef? _packageRef(String package) { + final current = entrypoint.lockFile.packages[package]; + if (current != null) return current.toRef(); + + for (final workspacePackage + in entrypoint.workspaceRoot.transitiveWorkspace) { + final dependency = workspacePackage.dependencies[package]; + if (dependency != null) return dependency.toRef(); + final devDependency = workspacePackage.devDependencies[package]; + if (devDependency != null) return devDependency.toRef(); + } + return null; + } + + _UpgradeTarget _parseUpgradeTarget(String argument) { + final parts = argument.split(':'); + if (parts.length > 2) { + usageException( + 'Could not parse upgrade target `$argument`. Use ``, ' + '`:latest`, or `:resolvable`.', + ); + } + + final package = parts.first; + if (!packageNameRegExp.hasMatch(package)) { + usageException('Not a valid package name: "$package"'); + } + + if (parts.length == 1) { + return _UpgradeTarget(argument, package, null); + } + + final suffix = parts.last; + final kind = switch (suffix) { + 'latest' => _UpgradeTargetKind.latest, + 'resolvable' => _UpgradeTargetKind.resolvable, + _ => null, + }; + if (kind == null) { + usageException( + 'Unknown upgrade target `$argument`. Use ``, ' + '`:latest`, or `:resolvable`.', + ); + } + return _UpgradeTarget(argument, package, kind); + } + /// Return names of packages to be upgraded, and throws [UsageException] if /// any package names not in the direct dependencies or dev_dependencies are /// given. @@ -240,21 +382,7 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: Future _runUpgradeMajorVersions() async { final toUpgrade = await _directDependenciesToUpgrade(); - // Solve [resolvablePubspec] in-memory and consolidate the resolved - // versions of the packages into a map for quick searching. - final resolvedPackages = {}; - final solveResult = await log.spinner('Resolving dependencies', () async { - return await resolveVersions( - SolveType.upgrade, - cache, - entrypoint.workspaceRoot.transformWorkspace( - (package) => stripVersionBounds(package.pubspec), - ), - ); - }, condition: _shouldShowSpinner); - for (final resolvedPackage in solveResult.packages) { - resolvedPackages[resolvedPackage.name] = resolvedPackage; - } + final resolvedPackages = await _latestResolvablePackages; final dependencyOverriddenDeps = []; // Changes to be made to `pubspec.yaml` of each package. // Mapping from original to changed value. @@ -377,3 +505,13 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: } } } + +enum _UpgradeTargetKind { latest, resolvable } + +class _UpgradeTarget { + final String argument; + final String name; + final _UpgradeTargetKind? kind; + + _UpgradeTarget(this.argument, this.name, this.kind); +} diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 314729a4453..68ee0e06735 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -35,6 +35,7 @@ import 'sdk/flutter.dart'; import 'solver.dart'; import 'solver/report.dart'; import 'solver/solve_suggestions.dart'; +import 'solver/version_solver.dart'; import 'source/cached.dart'; import 'source/hosted.dart'; import 'source/root.dart'; @@ -556,6 +557,10 @@ See $workspacesDocUrl for more information.''', /// The iterable [unlock] specifies the list of packages whose versions can be /// changed even if they are locked in the pubspec.lock file. /// + /// The iterable [additionalConstraints] specifies extra constraints the + /// version solver must satisfy. When omitted, no extra constraints are + /// applied. + /// /// Shows a report of the changes made relative to the previous lockfile. If /// this is an upgrade or downgrade, all transitive dependencies are shown in /// the report. Otherwise, only dependencies that were changed are shown. If @@ -575,6 +580,7 @@ See $workspacesDocUrl for more information.''', Future acquireDependencies( SolveType type, { Iterable unlock = const [], + Iterable? additionalConstraints, bool dryRun = false, bool precompile = false, bool summaryOnly = false, @@ -608,6 +614,7 @@ Try running `$topLevelProgram pub get` to create `$lockFilePath`.'''); workspaceRoot, lockFile: lockFile, unlock: unlock, + additionalConstraints: additionalConstraints, ); }); } on SolveFailure catch (e) { diff --git a/test/testdata/goldens/help_test/pub upgrade --help.txt b/test/testdata/goldens/help_test/pub upgrade --help.txt index 90030c7ca69..262edf55989 100644 --- a/test/testdata/goldens/help_test/pub upgrade --help.txt +++ b/test/testdata/goldens/help_test/pub upgrade --help.txt @@ -4,7 +4,12 @@ $ pub upgrade --help Upgrade the current package's dependencies to latest versions. -Usage: pub upgrade [dependencies...] +Append `:latest` to a dependency to require the latest available version. + +Append `:resolvable` to require the newest version resolvable with the rest of +the dependency graph. + +Usage: pub upgrade [dependencies[:latest|:resolvable]...] -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. diff --git a/test/upgrade/upgrade_transitive_test.dart b/test/upgrade/upgrade_transitive_test.dart index 05fea6d994a..5e92e6f25d6 100644 --- a/test/upgrade/upgrade_transitive_test.dart +++ b/test/upgrade/upgrade_transitive_test.dart @@ -5,6 +5,7 @@ @TestOn('vm') library; +import 'package:pub/src/exit_codes.dart' as exit_codes; import 'package:test/test.dart'; import '../descriptor.dart' as d; @@ -104,4 +105,162 @@ void main() { ), ); }); + + test('`dependency:latest` explains why latest cannot be selected', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '^1.0.0'}); + server.serve('bar', '1.0.0'); + + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubGet(output: contains('+ foo 1.0.0')); + + server.serve('bar', '2.0.0'); + + await pubUpgrade( + args: ['bar:latest'], + error: allOf( + contains('bar 2.0.0'), + contains('bar:latest'), + contains('foo'), + contains('bar ^1.0.0'), + contains('version solving failed'), + ), + ); + }); + + test( + '`dependency:latest` upgrades a transitive dependency to latest', + () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': 'any'}); + server.serve('bar', '1.0.0'); + + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubGet(output: contains('+ foo 1.0.0')); + + server.serve('bar', '2.0.0'); + + await pubUpgrade(args: ['bar:latest'], output: contains('> bar 2.0.0')); + }, + ); + + test('`dependency:resolvable` upgrades a transitive dependency to latest ' + 'resolvable', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '^1.0.0'}); + server.serve('bar', '1.0.0'); + + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubGet(output: contains('+ foo 1.0.0')); + + server.serve('bar', '1.5.0'); + server.serve('bar', '2.0.0'); + + await pubUpgrade( + args: ['bar:resolvable'], + output: allOf(contains('> bar 1.5.0'), isNot(contains('bar 2.0.0'))), + ); + }); + + test( + '`dependency:resolvable` explains why resolvable cannot be selected', + () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '^1.0.0'}); + server.serve('foo', '2.0.0', deps: {'bar': '^2.0.0'}); + server.serve('bar', '1.0.0'); + + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubGet(output: contains('+ foo 1.0.0')); + + server.serve('bar', '2.0.0'); + + await pubUpgrade( + args: ['bar:resolvable'], + error: allOf( + contains('bar 2.0.0'), + contains('bar:resolvable'), + contains('foo'), + contains('bar ^1.0.0'), + contains('version solving failed'), + ), + ); + }, + ); + + test('multiple dependency targets resolve together', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': 'any', 'baz': '^1.0.0'}); + server.serve('bar', '1.0.0'); + server.serve('baz', '1.0.0'); + + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubGet(output: contains('+ foo 1.0.0')); + + server.serve('foo', '1.5.0', deps: {'bar': 'any', 'baz': '^1.0.0'}); + server.serve('bar', '2.0.0'); + server.serve('baz', '1.5.0'); + server.serve('baz', '2.0.0'); + + await pubUpgrade( + args: ['foo', 'bar:latest', 'baz:resolvable'], + output: allOf( + contains('> foo 1.5.0'), + contains('> bar 2.0.0'), + contains('> baz 1.5.0'), + isNot(contains('baz 2.0.0')), + ), + ); + }); + + test( + '`dependency:latest` requires a package from the current resolution', + () async { + await servePackages(); + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubUpgrade( + args: ['missing:latest'], + error: contains('Package `missing` is not in the current resolution.'), + exitCode: exit_codes.DATA, + ); + }, + ); + + test( + '`dependency:latest` cannot be combined with --major-versions', + () async { + await servePackages(); + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubUpgrade( + args: ['--major-versions', 'foo:latest'], + error: contains( + 'Cannot use `:latest` or `:resolvable` with `--major-versions`.', + ), + exitCode: exit_codes.USAGE, + ); + }, + ); + + test('dependency target cannot contain multiple colons', () async { + await servePackages(); + await d.appDir(dependencies: {'foo': '^1.0.0'}).create(); + + await pubUpgrade( + args: ['foo:bar:latest'], + error: allOf( + contains('Could not parse upgrade target `foo:bar:latest`.'), + contains('Use ``'), + contains('`:latest`'), + contains('`:resolvable`.'), + ), + exitCode: exit_codes.USAGE, + ); + }); }