|
| 1 | +// Copyright 2013 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:file/file.dart'; |
| 6 | +import 'package:http/http.dart' as http; |
| 7 | +import 'package:pub_semver/pub_semver.dart'; |
| 8 | +import 'package:pubspec_parse/pubspec_parse.dart'; |
| 9 | +import 'package:yaml_edit/yaml_edit.dart'; |
| 10 | + |
| 11 | +import 'common/core.dart'; |
| 12 | +import 'common/package_looping_command.dart'; |
| 13 | +import 'common/pub_version_finder.dart'; |
| 14 | +import 'common/repository_package.dart'; |
| 15 | + |
| 16 | +const int _exitIncorrectTargetDependency = 3; |
| 17 | +const int _exitNoTargetVersion = 4; |
| 18 | + |
| 19 | +/// A command to update a dependency in packages. |
| 20 | +/// |
| 21 | +/// This is intended to expand over time to support any sort of dependency that |
| 22 | +/// packages use, including pub packages and native dependencies, and should |
| 23 | +/// include any tasks related to the dependency (e.g., regenerating files when |
| 24 | +/// updating a dependency that is responsible for code generation). |
| 25 | +class UpdateDependencyCommand extends PackageLoopingCommand { |
| 26 | + /// Creates an instance of the version check command. |
| 27 | + UpdateDependencyCommand( |
| 28 | + Directory packagesDir, { |
| 29 | + http.Client? httpClient, |
| 30 | + }) : _pubVersionFinder = |
| 31 | + PubVersionFinder(httpClient: httpClient ?? http.Client()), |
| 32 | + super(packagesDir) { |
| 33 | + argParser.addOption( |
| 34 | + _pubPackageFlag, |
| 35 | + help: 'A pub package to update.', |
| 36 | + ); |
| 37 | + argParser.addOption( |
| 38 | + _versionFlag, |
| 39 | + help: 'The version to update to.\n\n' |
| 40 | + '- For pub, defaults to the latest published version if not ' |
| 41 | + 'provided. This can be any constraint that pubspec.yaml allows; a ' |
| 42 | + 'specific version will be treated as the exact version for ' |
| 43 | + 'dependencies that are alread pinned, or a ^ range for those that ' |
| 44 | + 'are unpinned.', |
| 45 | + ); |
| 46 | + } |
| 47 | + |
| 48 | + static const String _pubPackageFlag = 'pub-package'; |
| 49 | + static const String _versionFlag = 'version'; |
| 50 | + |
| 51 | + final PubVersionFinder _pubVersionFinder; |
| 52 | + |
| 53 | + late final String? _targetPubPackage; |
| 54 | + late final String _targetVersion; |
| 55 | + |
| 56 | + @override |
| 57 | + final String name = 'update-dependency'; |
| 58 | + |
| 59 | + @override |
| 60 | + final String description = 'Updates a dependency in a package.'; |
| 61 | + |
| 62 | + @override |
| 63 | + bool get hasLongOutput => false; |
| 64 | + |
| 65 | + @override |
| 66 | + PackageLoopingType get packageLoopingType => |
| 67 | + PackageLoopingType.includeAllSubpackages; |
| 68 | + |
| 69 | + @override |
| 70 | + Future<void> initializeRun() async { |
| 71 | + const Set<String> targetFlags = <String>{_pubPackageFlag}; |
| 72 | + final Set<String> passedTargetFlags = |
| 73 | + targetFlags.where((String flag) => argResults![flag] != null).toSet(); |
| 74 | + if (passedTargetFlags.length != 1) { |
| 75 | + printError( |
| 76 | + 'Exactly one of the target flags must be provided: (${targetFlags.join(', ')})'); |
| 77 | + throw ToolExit(_exitIncorrectTargetDependency); |
| 78 | + } |
| 79 | + _targetPubPackage = getNullableStringArg(_pubPackageFlag); |
| 80 | + if (_targetPubPackage != null) { |
| 81 | + final String? version = getNullableStringArg(_versionFlag); |
| 82 | + if (version == null) { |
| 83 | + final PubVersionFinderResponse response = await _pubVersionFinder |
| 84 | + .getPackageVersion(packageName: _targetPubPackage!); |
| 85 | + switch (response.result) { |
| 86 | + case PubVersionFinderResult.success: |
| 87 | + _targetVersion = response.versions.first.toString(); |
| 88 | + break; |
| 89 | + case PubVersionFinderResult.fail: |
| 90 | + printError(''' |
| 91 | +Error fetching $_targetPubPackage version from pub: ${response.httpResponse.statusCode}: |
| 92 | +${response.httpResponse.body} |
| 93 | +'''); |
| 94 | + throw ToolExit(_exitNoTargetVersion); |
| 95 | + case PubVersionFinderResult.noPackageFound: |
| 96 | + printError('$_targetPubPackage does not exist on pub'); |
| 97 | + throw ToolExit(_exitNoTargetVersion); |
| 98 | + } |
| 99 | + } else { |
| 100 | + _targetVersion = version; |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + @override |
| 106 | + Future<void> completeRun() async { |
| 107 | + _pubVersionFinder.httpClient.close(); |
| 108 | + } |
| 109 | + |
| 110 | + @override |
| 111 | + Future<PackageResult> runForPackage(RepositoryPackage package) async { |
| 112 | + if (_targetPubPackage != null) { |
| 113 | + return _runForPubDependency(package, _targetPubPackage!); |
| 114 | + } |
| 115 | + // TODO(stuartmorgan): Add othe dependency types here (e.g., maven). |
| 116 | + |
| 117 | + return PackageResult.fail(); |
| 118 | + } |
| 119 | + |
| 120 | + /// Handles all of the updates for [package] when the target dependency is |
| 121 | + /// a pub dependency. |
| 122 | + Future<PackageResult> _runForPubDependency( |
| 123 | + RepositoryPackage package, String dependency) async { |
| 124 | + final _PubDependencyInfo? dependencyInfo = |
| 125 | + _getPubDependencyInfo(package, dependency); |
| 126 | + if (dependencyInfo == null) { |
| 127 | + return PackageResult.skip('Does not depend on $dependency'); |
| 128 | + } else if (!dependencyInfo.hosted) { |
| 129 | + return PackageResult.skip('$dependency in not a hosted dependency'); |
| 130 | + } |
| 131 | + |
| 132 | + final String sectionKey = dependencyInfo.type == _PubDependencyType.dev |
| 133 | + ? 'dev_dependencies' |
| 134 | + : 'dependencies'; |
| 135 | + final String versionString; |
| 136 | + final VersionConstraint parsedConstraint = |
| 137 | + VersionConstraint.parse(_targetVersion); |
| 138 | + // If the provided string was a constraint, or if it's a specific |
| 139 | + // version but the package has a pinned dependency, use it as-is. |
| 140 | + if (dependencyInfo.pinned || |
| 141 | + parsedConstraint is! VersionRange || |
| 142 | + parsedConstraint.min != parsedConstraint.max) { |
| 143 | + versionString = _targetVersion; |
| 144 | + } else { |
| 145 | + // Otherwise, it's a specific version; treat it as '^version'. |
| 146 | + final Version minVersion = parsedConstraint.min!; |
| 147 | + versionString = '^$minVersion'; |
| 148 | + } |
| 149 | + |
| 150 | + print('${indentation}Updating to "$versionString"'); |
| 151 | + if (versionString == dependencyInfo.constraintString) { |
| 152 | + return PackageResult.skip('Already depends on $versionString'); |
| 153 | + } |
| 154 | + final YamlEditor editablePubspec = |
| 155 | + YamlEditor(package.pubspecFile.readAsStringSync()); |
| 156 | + editablePubspec.update( |
| 157 | + <String>[sectionKey, dependency], |
| 158 | + versionString, |
| 159 | + ); |
| 160 | + package.pubspecFile.writeAsStringSync(editablePubspec.toString()); |
| 161 | + |
| 162 | + // TODO(stuartmorgan): Add additionally handling of known packages that |
| 163 | + // do file generation (mockito, pigeon, etc.). |
| 164 | + |
| 165 | + return PackageResult.success(); |
| 166 | + } |
| 167 | + |
| 168 | + /// Returns information about the current dependency of [package] on |
| 169 | + /// the package named [dependencyName], or null if there is no dependency. |
| 170 | + _PubDependencyInfo? _getPubDependencyInfo( |
| 171 | + RepositoryPackage package, String dependencyName) { |
| 172 | + final Pubspec pubspec = package.parsePubspec(); |
| 173 | + |
| 174 | + Dependency? dependency; |
| 175 | + final _PubDependencyType type; |
| 176 | + if (pubspec.dependencies.containsKey(dependencyName)) { |
| 177 | + dependency = pubspec.dependencies[dependencyName]; |
| 178 | + type = _PubDependencyType.normal; |
| 179 | + } else if (pubspec.devDependencies.containsKey(dependencyName)) { |
| 180 | + dependency = pubspec.devDependencies[dependencyName]; |
| 181 | + type = _PubDependencyType.dev; |
| 182 | + } else { |
| 183 | + return null; |
| 184 | + } |
| 185 | + if (dependency != null && dependency is HostedDependency) { |
| 186 | + final VersionConstraint version = dependency.version; |
| 187 | + return _PubDependencyInfo( |
| 188 | + type, |
| 189 | + pinned: version is VersionRange && version.min == version.max, |
| 190 | + hosted: true, |
| 191 | + constraintString: version.toString(), |
| 192 | + ); |
| 193 | + } |
| 194 | + return _PubDependencyInfo(type, pinned: false, hosted: false); |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +class _PubDependencyInfo { |
| 199 | + const _PubDependencyInfo(this.type, |
| 200 | + {required this.pinned, required this.hosted, this.constraintString}); |
| 201 | + final _PubDependencyType type; |
| 202 | + final bool pinned; |
| 203 | + final bool hosted; |
| 204 | + final String? constraintString; |
| 205 | +} |
| 206 | + |
| 207 | +enum _PubDependencyType { normal, dev } |
0 commit comments