| // 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 'dart:io'; |
| |
| 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 '../exceptions.dart'; |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../null_safety_analysis.dart'; |
| import '../package.dart'; |
| import '../package_name.dart'; |
| import '../pubspec.dart'; |
| import '../pubspec_utils.dart'; |
| import '../solver.dart'; |
| import '../source/hosted.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['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', |
| 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( |
| 'major-versions', |
| help: 'Upgrades packages to their latest resolvable versions, ' |
| 'and updates pubspec.yaml.', |
| negatable: false, |
| ); |
| |
| argParser.addFlag( |
| 'example', |
| 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 => stdout.hasTerminal; |
| |
| bool get _dryRun => argResults['dry-run']; |
| |
| bool get _precompile => argResults['precompile']; |
| |
| bool get _upgradeNullSafety => |
| argResults['nullsafety'] || argResults['null-safety']; |
| |
| bool get _upgradeMajorVersions => argResults['major-versions']; |
| |
| @override |
| Future<void> runProtected() async { |
| if (argResults.wasParsed('packages-dir')) { |
| log.warning(log.yellow( |
| 'The --packages-dir flag is no longer used and does nothing.')); |
| } |
| |
| if (_upgradeNullSafety && _upgradeMajorVersions) { |
| usageException('--major-versions and --null-safety cannot be combined'); |
| } |
| |
| if (_upgradeNullSafety) { |
| if (argResults['example'] && entrypoint.example != null) { |
| log.warning( |
| 'Running `upgrade --null-safety` only in `${entrypoint.root.dir}`. Run `$topLevelProgram pub upgrade --null-safety --directory example/` separately.'); |
| } |
| await _runUpgradeNullSafety(); |
| } else if (_upgradeMajorVersions) { |
| if (argResults['example'] && entrypoint.example != null) { |
| log.warning( |
| 'Running `upgrade --major-versions` only in `${entrypoint.root.dir}`. Run `$topLevelProgram pub upgrade --major-versions --directory example/` separately.'); |
| } |
| await _runUpgradeMajorVersions(); |
| } else { |
| await _runUpgrade(entrypoint); |
| } |
| if (argResults['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: argResults.rest, |
| dryRun: _dryRun, |
| precompile: _precompile, |
| onlyReportSuccessOrFailure: onlySummary, |
| analytics: analytics, |
| ); |
| _showOfflineWarning(); |
| } |
| |
| /// 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 either `--major-versions` or `--null-safety` was passed. |
| List<String> _directDependenciesToUpgrade() { |
| assert(_upgradeNullSafety || _upgradeMajorVersions); |
| |
| final directDeps = [ |
| ...entrypoint.root.pubspec.dependencies.keys, |
| ...entrypoint.root.pubspec.devDependencies.keys |
| ]; |
| final toUpgrade = argResults.rest.isEmpty ? directDeps : argResults.rest; |
| |
| // Check that all package names in upgradeOnly are direct-dependencies |
| final notInDeps = toUpgrade.where((n) => !directDeps.contains(n)); |
| if (toUpgrade.any(notInDeps.contains)) { |
| var modeFlag = ''; |
| if (_upgradeNullSafety) { |
| modeFlag = '--null-safety'; |
| } |
| if (_upgradeMajorVersions) { |
| modeFlag = '--major-versions'; |
| } |
| |
| usageException(''' |
| Dependencies specified in `$topLevelProgram pub upgrade $modeFlag <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 resolvablePubspec = stripVersionUpperBounds( |
| entrypoint.root.pubspec, |
| stripOnly: toUpgrade, |
| ); |
| |
| // 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.inMemory(resolvablePubspec), |
| ); |
| }, condition: _shouldShowSpinner); |
| for (final resolvedPackage in solveResult.packages) { |
| resolvedPackages[resolvedPackage.name] = resolvedPackage; |
| } |
| |
| // Changes to be made to `pubspec.yaml`. |
| // Mapping from original to changed value. |
| final changes = <PackageRange, PackageRange>{}; |
| final declaredHostedDependencies = [ |
| ...entrypoint.root.pubspec.dependencies.values, |
| ...entrypoint.root.pubspec.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 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.root.dependencyOverrides.containsKey(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[dep] = dep.toRef().withConstraint( |
| VersionConstraint.compatibleWith( |
| resolvedPackage.version, |
| ), |
| ); |
| } |
| final newPubspecText = _updatePubspec(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 = |
| argResults.rest.isEmpty ? SolveType.upgrade : SolveType.get; |
| |
| if (_dryRun) { |
| // Even if it is a dry run, run `acquireDependencies` so that the user |
| // gets a report on changes. |
| await Entrypoint.inMemory( |
| Package.inMemory( |
| Pubspec.parse(newPubspecText, cache.sources), |
| ), |
| cache, |
| lockFile: entrypoint.lockFile, |
| solveResult: solveResult, |
| ).acquireDependencies( |
| solveType, |
| dryRun: true, |
| precompile: _precompile, |
| analytics: null, // No analytics for dry-run |
| ); |
| } else { |
| if (changes.isNotEmpty) { |
| writeTextFile(entrypoint.pubspecPath, newPubspecText); |
| } |
| // TODO: Allow Entrypoint to be created with in-memory pubspec, so that |
| // we can show the changes when not in --dry-run mode. For now we only show |
| // the changes made to pubspec.yaml in dry-run mode. |
| await Entrypoint(directory, cache).acquireDependencies( |
| solveType, |
| precompile: _precompile, |
| analytics: analytics, |
| ); |
| } |
| |
| _outputChangeSummary(changes); |
| |
| // If any of the packages to upgrade are dependency overrides, then we |
| // show a warning. |
| final toUpgradeOverrides = |
| toUpgrade.where(entrypoint.root.dependencyOverrides.containsKey); |
| if (toUpgradeOverrides.isNotEmpty) { |
| log.warning( |
| 'Warning: dependency_overrides prevents upgrades for: ' |
| '${toUpgradeOverrides.join(', ')}', |
| ); |
| } |
| |
| _showOfflineWarning(); |
| } |
| |
| Future<void> _runUpgradeNullSafety() async { |
| final toUpgrade = _directDependenciesToUpgrade(); |
| |
| final nullsafetyPubspec = await _upgradeToNullSafetyConstraints( |
| entrypoint.root.pubspec, |
| toUpgrade, |
| ); |
| |
| /// Solve [nullsafetyPubspec] 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.inMemory(nullsafetyPubspec), |
| ); |
| }, condition: _shouldShowSpinner); |
| for (final resolvedPackage in solveResult.packages) { |
| resolvedPackages[resolvedPackage.name] = resolvedPackage; |
| } |
| |
| /// Changes to be made to `pubspec.yaml`. |
| /// Mapping from original to changed value. |
| final changes = <PackageRange, PackageRange>{}; |
| final declaredHostedDependencies = [ |
| ...entrypoint.root.pubspec.dependencies.values, |
| ...entrypoint.root.pubspec.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 to upgrade this package, or it wasn't in the |
| // resolution somehow, then we ignore it. |
| continue; |
| } |
| |
| final constraint = VersionConstraint.compatibleWith( |
| resolvedPackage.version, |
| ); |
| if (dep.constraint.allowsAll(constraint) && |
| constraint.allowsAll(dep.constraint)) { |
| // If constraint allows the same as the existing constraint then |
| // there is no need to make changes. |
| continue; |
| } |
| |
| changes[dep] = dep.toRef().withConstraint(constraint); |
| } |
| |
| final newPubspecText = _updatePubspec(changes); |
| if (_dryRun) { |
| // Even if it is a dry run, run `acquireDependencies` so that the user |
| // gets a report on changes. |
| // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output |
| await Entrypoint.inMemory( |
| Package.inMemory(Pubspec.parse(newPubspecText, cache.sources)), |
| cache, |
| lockFile: entrypoint.lockFile, |
| solveResult: solveResult, |
| ).acquireDependencies( |
| SolveType.upgrade, |
| dryRun: true, |
| precompile: _precompile, |
| analytics: null, |
| ); |
| } else { |
| if (changes.isNotEmpty) { |
| writeTextFile(entrypoint.pubspecPath, newPubspecText); |
| } |
| // TODO: Allow Entrypoint to be created with in-memory pubspec, so that |
| // we can show the changes in --dry-run mode. For now we only show |
| // the changes made to pubspec.yaml in dry-run mode. |
| await Entrypoint(directory, cache).acquireDependencies( |
| SolveType.upgrade, |
| precompile: _precompile, |
| analytics: analytics, |
| ); |
| } |
| |
| _outputChangeSummary(changes); |
| |
| // Warn if not all dependencies were migrated to a null-safety compatible |
| // version. This can happen because: |
| // - `upgradeOnly` was given, |
| // - root has SDK dependencies, |
| // - root has git or path dependencies, |
| // - root has dependency_overrides |
| final nonMigratedDirectDeps = <String>[]; |
| final directDeps = [ |
| ...entrypoint.root.pubspec.dependencies.keys, |
| ...entrypoint.root.pubspec.devDependencies.keys |
| ]; |
| await Future.wait(directDeps.map((name) async { |
| final resolvedPackage = resolvedPackages[name]!; |
| |
| final pubspec = await cache.describe(resolvedPackage); |
| if (!pubspec.languageVersion.supportsNullSafety) { |
| nonMigratedDirectDeps.add(name); |
| } |
| })); |
| if (nonMigratedDirectDeps.isNotEmpty) { |
| log.warning(''' |
| \nFollowing direct 'dependencies' and 'dev_dependencies' are not migrated to |
| null-safety yet: |
| - ${nonMigratedDirectDeps.join('\n - ')} |
| |
| You may have to: |
| * Upgrade git and path dependencies manually, |
| * Upgrade to a newer SDK for newer SDK dependencies, |
| * Remove dependency_overrides, and/or, |
| * Find other packages to use. |
| '''); |
| } |
| |
| _showOfflineWarning(); |
| } |
| |
| /// Updates `pubspec.yaml` with given [changes]. |
| String _updatePubspec( |
| Map<PackageRange, PackageRange> changes, |
| ) { |
| ArgumentError.checkNotNull(changes, 'changes'); |
| |
| final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath)); |
| final deps = entrypoint.root.pubspec.dependencies.keys; |
| final devDeps = entrypoint.root.pubspec.devDependencies.keys; |
| |
| for (final change in changes.values) { |
| if (deps.contains(change.name)) { |
| yamlEditor.update( |
| ['dependencies', change.name], |
| // TODO(jonasfj): Fix support for third-party pub servers. |
| change.constraint.toString(), |
| ); |
| } else if (devDeps.contains(change.name)) { |
| yamlEditor.update( |
| ['dev_dependencies', change.name], |
| // TODO: Fix support for third-party pub servers |
| change.constraint.toString(), |
| ); |
| } |
| } |
| return yamlEditor.toString(); |
| } |
| |
| /// Outputs a summary of changes made to `pubspec.yaml`. |
| void _outputChangeSummary( |
| Map<PackageRange, PackageRange> changes, |
| ) { |
| ArgumentError.checkNotNull(changes, 'changes'); |
| |
| if (changes.isEmpty) { |
| final wouldBe = _dryRun ? 'would be made to' : 'to'; |
| log.message('\nNo changes $wouldBe pubspec.yaml!'); |
| } else { |
| final s = changes.length == 1 ? '' : 's'; |
| |
| final changed = _dryRun ? 'Would change' : 'Changed'; |
| log.message('\n$changed ${changes.length} constraint$s in pubspec.yaml:'); |
| changes.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.'); |
| } |
| } |
| |
| /// Returns new pubspec with the same dependencies as [original], but with: |
| /// * the lower-bound of hosted package constraint set to first null-safety |
| /// compatible version, and, |
| /// * the upper-bound of hosted package constraints removed. |
| /// |
| /// Only changes listed in [upgradeOnly] will have their constraints touched. |
| /// |
| /// Throws [ApplicationException] if one of the dependencies does not have |
| /// a null-safety compatible version. |
| Future<Pubspec> _upgradeToNullSafetyConstraints( |
| Pubspec original, |
| List<String> upgradeOnly, |
| ) async { |
| ArgumentError.checkNotNull(original, 'original'); |
| ArgumentError.checkNotNull(upgradeOnly, 'upgradeOnly'); |
| |
| final hasNoNullSafetyVersions = <String>{}; |
| final hasNullSafetyVersions = <String>{}; |
| |
| Future<Iterable<PackageRange>> removeUpperConstraints( |
| Iterable<PackageRange> dependencies, |
| ) async => |
| await Future.wait(dependencies.map((dep) async { |
| if (dep.source is! HostedSource) { |
| return dep; |
| } |
| if (!upgradeOnly.contains(dep.name)) { |
| return dep; |
| } |
| |
| final packages = await cache.getVersions(dep.toRef()); |
| packages.sort((a, b) => a.version.compareTo(b.version)); |
| |
| for (final package in packages) { |
| final pubspec = await cache.describe(package); |
| if (pubspec.languageVersion.supportsNullSafety) { |
| hasNullSafetyVersions.add(dep.name); |
| return dep.toRef().withConstraint( |
| VersionRange(min: package.version, includeMin: true), |
| ); |
| } |
| } |
| |
| hasNoNullSafetyVersions.add(dep.name); |
| // This value is never used. We will throw an exception because |
| //`hasNonNullSafetyVersions` is not empty. |
| return dep.toRef().withConstraint(VersionConstraint.empty); |
| })); |
| |
| final deps = removeUpperConstraints(original.dependencies.values); |
| final devDeps = removeUpperConstraints(original.devDependencies.values); |
| await Future.wait([deps, devDeps]); |
| |
| if (hasNoNullSafetyVersions.isNotEmpty) { |
| throw ApplicationException(''' |
| null-safety compatible versions do not exist for: |
| - ${hasNoNullSafetyVersions.join('\n - ')} |
| |
| You can choose to upgrade only some dependencies to null-safety using: |
| $topLevelProgram pub upgrade --nullsafety ${hasNullSafetyVersions.join(' ')} |
| |
| Warning: Using null-safety features before upgrading all dependencies is |
| discouraged. For more details see: ${NullSafetyAnalysis.guideUrl} |
| '''); |
| } |
| |
| return Pubspec( |
| original.name, |
| version: original.version, |
| sdkConstraints: original.sdkConstraints, |
| dependencies: await deps, |
| devDependencies: await devDeps, |
| dependencyOverrides: original.dependencyOverrides.values, |
| ); |
| } |
| } |