| // Copyright (c) 2012, 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 'package:pub_semver/pub_semver.dart'; |
| import 'package:yaml_edit/yaml_edit.dart'; |
| |
| import '../command.dart'; |
| import '../command_runner.dart'; |
| import '../entrypoint.dart'; |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../package.dart'; |
| import '../package_name.dart'; |
| import '../pubspec.dart'; |
| import '../pubspec_utils.dart'; |
| import '../sdk.dart'; |
| import '../solver.dart'; |
| import '../source/hosted.dart'; |
| import '../utils.dart'; |
| |
| /// Handles the `upgrade` pub command. |
| class UpgradeCommand extends PubCommand { |
| @override |
| String get name => 'upgrade'; |
| @override |
| String get description => |
| "Upgrade the current package's dependencies to latest versions."; |
| @override |
| String get argumentsDescription => '[dependencies...]'; |
| @override |
| String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-upgrade'; |
| |
| @override |
| bool get isOffline => argResults.flag('offline'); |
| |
| UpgradeCommand() { |
| argParser.addFlag( |
| 'offline', |
| help: 'Use cached packages instead of accessing the network.', |
| ); |
| |
| argParser.addFlag( |
| 'dry-run', |
| abbr: 'n', |
| negatable: false, |
| help: "Report what dependencies would change but don't change any.", |
| ); |
| |
| argParser.addFlag( |
| 'precompile', |
| help: 'Precompile executables in immediate dependencies.', |
| ); |
| |
| argParser.addFlag( |
| 'null-safety', |
| hide: true, |
| negatable: false, |
| help: 'Upgrade constraints in pubspec.yaml to null-safety versions', |
| ); |
| argParser.addFlag('nullsafety', negatable: false, hide: true); |
| |
| argParser.addFlag('packages-dir', hide: true); |
| |
| argParser.addFlag( |
| 'tighten', |
| help: |
| 'Updates lower bounds in pubspec.yaml to match the resolved version.', |
| negatable: false, |
| ); |
| |
| argParser.addFlag( |
| 'major-versions', |
| help: 'Upgrades packages to their latest resolvable versions, ' |
| 'and updates pubspec.yaml.', |
| negatable: false, |
| ); |
| |
| argParser.addFlag( |
| 'example', |
| defaultsTo: true, |
| help: 'Also run in `example/` (if it exists).', |
| hide: true, |
| ); |
| |
| argParser.addOption( |
| 'directory', |
| abbr: 'C', |
| help: 'Run this in the directory <dir>.', |
| valueHelp: 'dir', |
| ); |
| } |
| |
| /// Avoid showing spinning progress messages when not in a terminal. |
| bool get _shouldShowSpinner => terminalOutputForStdout; |
| |
| bool get _dryRun => argResults.flag('dry-run'); |
| |
| bool get _tighten => argResults.flag('tighten'); |
| |
| bool get _precompile => argResults.flag('precompile'); |
| |
| /// 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 |
| /// upgrade command to affect. |
| List<String> get _packagesToUpgrade => argResults.rest; |
| |
| bool get _upgradeNullSafety => |
| argResults.flag('nullsafety') || argResults.flag('null-safety'); |
| |
| bool get _upgradeMajorVersions => argResults.flag('major-versions'); |
| |
| @override |
| Future<void> runProtected() async { |
| if (_upgradeNullSafety) { |
| dataError('''The `--null-safety` flag is no longer supported. |
| Consider using the Dart 2.19 sdk to migrate to null safety.'''); |
| } |
| if (argResults.wasParsed('packages-dir')) { |
| log.warning( |
| log.yellow( |
| 'The --packages-dir flag is no longer used and does nothing.', |
| ), |
| ); |
| } |
| |
| if (_upgradeMajorVersions) { |
| if (argResults.flag('example') && entrypoint.example != null) { |
| log.warning( |
| 'Running `upgrade --major-versions` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --major-versions --directory example/` separately.', |
| ); |
| } |
| await _runUpgradeMajorVersions(); |
| } else { |
| await _runUpgrade(entrypoint); |
| if (_tighten) { |
| final changes = tighten( |
| entrypoint, |
| entrypoint.lockFile.packages.values.toList(), |
| ); |
| if (!_dryRun) { |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { |
| final changesForPackage = changes[package]; |
| if (changesForPackage == null || changesForPackage.isEmpty) { |
| continue; |
| } |
| final newPubspecText = |
| _updatePubspecText(package, changesForPackage); |
| |
| if (changes.isNotEmpty) { |
| writeTextFile(package.pubspecPath, newPubspecText); |
| } |
| } |
| } |
| _outputChangeSummary(changes); |
| } |
| } |
| if (argResults.flag('example') && entrypoint.example != null) { |
| // Reload the entrypoint to ensure we pick up potential changes that has |
| // been made. |
| final exampleEntrypoint = Entrypoint(directory, cache).example!; |
| await _runUpgrade(exampleEntrypoint, onlySummary: true); |
| } |
| } |
| |
| Future<void> _runUpgrade(Entrypoint e, {bool onlySummary = false}) async { |
| await e.acquireDependencies( |
| SolveType.upgrade, |
| unlock: _packagesToUpgrade, |
| dryRun: _dryRun, |
| precompile: _precompile, |
| summaryOnly: onlySummary, |
| ); |
| |
| _showOfflineWarning(); |
| } |
| |
| /// Returns a list of changes to constraints in [pubspec] updated them to |
| /// have their lower bound match the version in [packages]. |
| /// |
| /// The return value is a mapping from the original package range to the updated. |
| /// |
| /// If packages to update where given in [_packagesToUpgrade], only those are |
| /// tightened. Otherwise all packages are tightened. |
| /// |
| /// If a dependency has already been updated in [existingChanges], the update |
| /// will apply on top of that change (eg. preserving the new upper bound). |
| Map<Package, Map<PackageRange, PackageRange>> tighten( |
| Entrypoint entrypoint, |
| List<PackageId> packages, { |
| Map<Package, Map<PackageRange, PackageRange>> existingChanges = const {}, |
| }) { |
| final result = {...existingChanges}; |
| if (argResults.flag('example') && entrypoint.example != null) { |
| log.warning( |
| 'Running `upgrade --tighten` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.', |
| ); |
| } |
| |
| final toTighten = <(Package, PackageRange)>[]; |
| |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { |
| if (_packagesToUpgrade.isEmpty) { |
| for (final range in [ |
| ...package.dependencies.values, |
| ...package.devDependencies.values, |
| ]) { |
| toTighten.add((package, range)); |
| } |
| } else { |
| for (final packageToUpgrade in _packagesToUpgrade) { |
| final range = package.dependencies[packageToUpgrade] ?? |
| package.devDependencies[packageToUpgrade]; |
| if (range != null) { |
| toTighten.add((package, range)); |
| } |
| } |
| } |
| } |
| |
| for (final (package, range) in toTighten) { |
| final changesForPackage = result[package] ??= {}; |
| final constraint = (changesForPackage[range] ?? range).constraint; |
| final resolvedVersion = |
| packages.firstWhere((p) => p.name == range.name).version; |
| if (range.source is HostedSource && constraint.isAny) { |
| changesForPackage[range] = range |
| .toRef() |
| .withConstraint(VersionConstraint.compatibleWith(resolvedVersion)); |
| } else if (constraint is VersionRange) { |
| final min = constraint.min; |
| if (min != null && min < resolvedVersion) { |
| changesForPackage[range] = range.toRef().withConstraint( |
| VersionRange( |
| min: resolvedVersion, |
| max: constraint.max, |
| includeMin: true, |
| includeMax: constraint.includeMax, |
| ).asCompatibleWithIfPossible(), |
| ); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /// Return names of packages to be upgraded, and throws [UsageException] if |
| /// any package names not in the direct dependencies or dev_dependencies are given. |
| /// |
| /// This assumes that `--major-versions` was passed. |
| List<String> _directDependenciesToUpgrade() { |
| assert(_upgradeMajorVersions); |
| |
| final directDeps = { |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) ...[ |
| ...package.dependencies.keys, |
| ...package.devDependencies.keys, |
| ], |
| }.toList(); |
| final toUpgrade = |
| _packagesToUpgrade.isEmpty ? directDeps : _packagesToUpgrade; |
| |
| // Check that all package names in upgradeOnly are direct-dependencies |
| final notInDeps = toUpgrade.where((n) => !directDeps.contains(n)); |
| if (toUpgrade.any(notInDeps.contains)) { |
| usageException(''' |
| Dependencies specified in `$topLevelProgram pub upgrade --major-versions <dependencies>` must |
| be direct 'dependencies' or 'dev_dependencies', following packages are not: |
| - ${notInDeps.join('\n - ')} |
| |
| '''); |
| } |
| |
| return toUpgrade; |
| } |
| |
| Future<void> _runUpgradeMajorVersions() async { |
| final toUpgrade = _directDependenciesToUpgrade(); |
| final workspace = { |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) |
| package.dir: package, |
| }; |
| // Solve [resolvablePubspec] in-memory and consolidate the resolved |
| // versions of the packages into a map for quick searching. |
| final resolvedPackages = <String, PackageId>{}; |
| final solveResult = await log.spinner( |
| 'Resolving dependencies', |
| () async { |
| return await resolveVersions( |
| SolveType.upgrade, |
| cache, |
| Package.load( |
| entrypoint.workspaceRoot.dir, |
| entrypoint.cache.sources, |
| withPubspecOverrides: true, |
| loadPubspec: ( |
| path, { |
| expectedName, |
| required withPubspecOverrides, |
| }) => |
| stripVersionBounds(workspace[path]!.pubspec), |
| ), |
| ); |
| }, |
| condition: _shouldShowSpinner, |
| ); |
| for (final resolvedPackage in solveResult.packages) { |
| resolvedPackages[resolvedPackage.name] = resolvedPackage; |
| } |
| final dependencyOverriddenDeps = <String>[]; |
| // Changes to be made to `pubspec.yaml` of each package. |
| // Mapping from original to changed value. |
| var changes = <Package, Map<PackageRange, PackageRange>>{}; |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { |
| final declaredHostedDependencies = [ |
| ...package.dependencies.values, |
| ...package.devDependencies.values, |
| ].where((dep) => dep.source is HostedSource); |
| for (final dep in declaredHostedDependencies) { |
| final resolvedPackage = resolvedPackages[dep.name]!; |
| if (!toUpgrade.contains(dep.name)) { |
| // If we're not trying to upgrade this package, or it wasn't in the |
| // resolution somehow, then we ignore it. |
| continue; |
| } |
| |
| // Skip [dep] if it has a dependency_override. |
| if (entrypoint.workspaceRoot.dependencyOverrides |
| .containsKey(dep.name)) { |
| dependencyOverriddenDeps.add(dep.name); |
| continue; |
| } |
| |
| if (dep.constraint.allowsAll(resolvedPackage.version)) { |
| // If constraint allows the resolvable version we found, then there is |
| // no need to update the `pubspec.yaml` |
| continue; |
| } |
| |
| (changes[package] ??= {})[dep] = dep.toRef().withConstraint( |
| VersionConstraint.compatibleWith(resolvedPackage.version), |
| ); |
| } |
| } |
| |
| if (_tighten) { |
| // Do another solve with the updated constraints to obtain the correct |
| // versions to tighten to. This should be fast (everything is cached, and |
| // no backtracking needed) so we don't show a spinner. |
| |
| final solveResult = await resolveVersions( |
| SolveType.upgrade, |
| cache, |
| Package.load( |
| entrypoint.workspaceRoot.dir, |
| entrypoint.cache.sources, |
| loadPubspec: (path, {expectedName, required withPubspecOverrides}) { |
| final package = workspace[path]!; |
| final changesForPackage = changes[package] ?? {}; |
| return applyChanges(package.pubspec, changesForPackage); |
| }, |
| ), |
| ); |
| changes = tighten( |
| entrypoint, |
| solveResult.packages, |
| existingChanges: changes, |
| ); |
| } |
| |
| // When doing '--majorVersions' for specific packages we try to update other |
| // packages as little as possible to make a focused change (SolveType.get). |
| // |
| // But without a specific package we want to get as many non-major updates |
| // as possible (SolveType.upgrade). |
| final solveType = |
| _packagesToUpgrade.isEmpty ? SolveType.upgrade : SolveType.get; |
| |
| if (!_dryRun) { |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { |
| final changesForPackage = changes[package] ?? {}; |
| if (changesForPackage.isNotEmpty) { |
| final newPubspecText = _updatePubspecText(package, changesForPackage); |
| writeTextFile(package.pubspecPath, newPubspecText); |
| } |
| } |
| } |
| await entrypoint.withUpdatedPubspecs({ |
| for (final MapEntry(key: package, value: changesForPackage) |
| in changes.entries) |
| package: applyChanges(package.pubspec, changesForPackage), |
| }).acquireDependencies( |
| solveType, |
| dryRun: _dryRun, |
| precompile: !_dryRun && _precompile, |
| ); |
| |
| _outputChangeSummary(changes); |
| |
| // If any of the packages to upgrade are dependency overrides, then we |
| // show a warning. |
| final toUpgradeOverrides = toUpgrade |
| .where(entrypoint.workspaceRoot.dependencyOverrides.containsKey); |
| if (toUpgradeOverrides.isNotEmpty) { |
| log.warning( |
| 'Warning: dependency_overrides prevents upgrades for: ' |
| '${toUpgradeOverrides.join(', ')}', |
| ); |
| } |
| |
| _showOfflineWarning(); |
| } |
| |
| Pubspec applyChanges( |
| Pubspec original, |
| Map<PackageRange, PackageRange> changes, |
| ) { |
| final dependencies = {...original.dependencies}; |
| final devDependencies = {...original.devDependencies}; |
| |
| for (final change in changes.values) { |
| if (dependencies[change.name] != null) { |
| dependencies[change.name] = change; |
| } else { |
| devDependencies[change.name] = change; |
| } |
| } |
| return original.copyWith( |
| dependencies: dependencies.values, |
| devDependencies: devDependencies.values, |
| ); |
| } |
| |
| /// Loads `pubspec.yaml` of [package] and applies [changes] to its |
| /// (dev)-dependencies. |
| /// |
| /// Returns the updated textual representation using yaml-edit to preserve |
| /// structure. |
| String _updatePubspecText( |
| Package package, |
| Map<PackageRange, PackageRange> changes, |
| ) { |
| ArgumentError.checkNotNull(changes, 'changes'); |
| final yamlEditor = YamlEditor(readTextFile(package.pubspecPath)); |
| final deps = package.dependencies.keys; |
| |
| for (final change in changes.values) { |
| final section = |
| deps.contains(change.name) ? 'dependencies' : 'dev_dependencies'; |
| yamlEditor.update( |
| [section, change.name], |
| pubspecDescription(change, cache, package), |
| ); |
| } |
| return yamlEditor.toString(); |
| } |
| |
| /// Outputs a summary of changes made to `pubspec.yaml`. |
| void _outputChangeSummary( |
| Map<Package, Map<PackageRange, PackageRange>> changes, |
| ) { |
| if (entrypoint.workspaceRoot.workspaceChildren.isEmpty) { |
| final changesToWorkspaceRoot = changes[entrypoint.workspaceRoot] ?? {}; |
| if (changesToWorkspaceRoot.isEmpty) { |
| final wouldBe = _dryRun ? 'would be made to' : 'to'; |
| log.message('\nNo changes $wouldBe pubspec.yaml!'); |
| } else { |
| final changed = _dryRun ? 'Would change' : 'Changed'; |
| log.message('\n$changed ${changesToWorkspaceRoot.length} ' |
| '${pluralize('constraint', changesToWorkspaceRoot.length)} in pubspec.yaml:'); |
| changesToWorkspaceRoot.forEach((from, to) { |
| log.message(' ${from.name}: ${from.constraint} -> ${to.constraint}'); |
| }); |
| } |
| } else { |
| if (changes.isEmpty) { |
| final wouldBe = _dryRun ? 'would be made to' : 'to'; |
| log.message('\nNo changes $wouldBe any pubspec.yaml!'); |
| } |
| for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { |
| final changesToPackage = changes[package] ?? {}; |
| if (changesToPackage.isEmpty) continue; |
| final changed = _dryRun ? 'Would change' : 'Changed'; |
| log.message('\n$changed ${changesToPackage.length} ' |
| '${pluralize('constraint', changesToPackage.length)} in ${package.pubspecPath}:'); |
| changesToPackage.forEach((from, to) { |
| log.message(' ${from.name}: ${from.constraint} -> ${to.constraint}'); |
| }); |
| } |
| } |
| } |
| |
| void _showOfflineWarning() { |
| if (isOffline) { |
| log.warning('Warning: Upgrading when offline may not update you to the ' |
| 'latest versions of your dependencies.'); |
| } |
| } |
| } |