[tool] Add initial update-dependency command (#3632)

[tool] Add initial `update-dependency` command
This commit is contained in:
stuartmorgan
2023-04-03 14:23:11 -07:00
committed by GitHub
parent 1cac25da9d
commit 3df3ba528e
4 changed files with 572 additions and 0 deletions

View File

@ -135,6 +135,26 @@ the branch point from `upstream/main`. For more complex use cases where you want
a different diff point, you can pass a different `--base-branch`, or use
`--base-sha` to pick the exact diff point.
### Update a dependency
`update-dependency` will update a pub dependency to a new version.
For instance, to updated to version 3.0.0 of `some_package` in every package
that depends on it:
```sh
cd <repository root>
dart run script/tool/bin/flutter_plugin_tools.dart update-dependency \
--pub-package=some_package \
--version=3.0.0 \
```
If a `--version` is not provided, the latest version from pub will be used.
Currently this only updates the dependency itself in pubspec.yaml, but in the
future this will also update any generated code for packages that use code
generation (e.g., regenerating mocks when updating `mockito`).
### Publish a Release
**Releases are automated for `flutter/packages`.**

View File

@ -31,6 +31,7 @@ import 'pubspec_check_command.dart';
import 'readme_check_command.dart';
import 'remove_dev_dependencies_command.dart';
import 'test_command.dart';
import 'update_dependency_command.dart';
import 'update_excerpts_command.dart';
import 'update_min_sdk_command.dart';
import 'update_release_info_command.dart';
@ -77,6 +78,7 @@ void main(List<String> args) {
..addCommand(ReadmeCheckCommand(packagesDir))
..addCommand(RemoveDevDependenciesCommand(packagesDir))
..addCommand(TestCommand(packagesDir))
..addCommand(UpdateDependencyCommand(packagesDir))
..addCommand(UpdateExcerptsCommand(packagesDir))
..addCommand(UpdateMinSdkCommand(packagesDir))
..addCommand(UpdateReleaseInfoCommand(packagesDir))

View File

@ -0,0 +1,207 @@
// Copyright 2013 The Flutter Authors. 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:file/file.dart';
import 'package:http/http.dart' as http;
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:yaml_edit/yaml_edit.dart';
import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/pub_version_finder.dart';
import 'common/repository_package.dart';
const int _exitIncorrectTargetDependency = 3;
const int _exitNoTargetVersion = 4;
/// A command to update a dependency in packages.
///
/// This is intended to expand over time to support any sort of dependency that
/// packages use, including pub packages and native dependencies, and should
/// include any tasks related to the dependency (e.g., regenerating files when
/// updating a dependency that is responsible for code generation).
class UpdateDependencyCommand extends PackageLoopingCommand {
/// Creates an instance of the version check command.
UpdateDependencyCommand(
Directory packagesDir, {
http.Client? httpClient,
}) : _pubVersionFinder =
PubVersionFinder(httpClient: httpClient ?? http.Client()),
super(packagesDir) {
argParser.addOption(
_pubPackageFlag,
help: 'A pub package to update.',
);
argParser.addOption(
_versionFlag,
help: 'The version to update to.\n\n'
'- For pub, defaults to the latest published version if not '
'provided. This can be any constraint that pubspec.yaml allows; a '
'specific version will be treated as the exact version for '
'dependencies that are alread pinned, or a ^ range for those that '
'are unpinned.',
);
}
static const String _pubPackageFlag = 'pub-package';
static const String _versionFlag = 'version';
final PubVersionFinder _pubVersionFinder;
late final String? _targetPubPackage;
late final String _targetVersion;
@override
final String name = 'update-dependency';
@override
final String description = 'Updates a dependency in a package.';
@override
bool get hasLongOutput => false;
@override
PackageLoopingType get packageLoopingType =>
PackageLoopingType.includeAllSubpackages;
@override
Future<void> initializeRun() async {
const Set<String> targetFlags = <String>{_pubPackageFlag};
final Set<String> passedTargetFlags =
targetFlags.where((String flag) => argResults![flag] != null).toSet();
if (passedTargetFlags.length != 1) {
printError(
'Exactly one of the target flags must be provided: (${targetFlags.join(', ')})');
throw ToolExit(_exitIncorrectTargetDependency);
}
_targetPubPackage = getNullableStringArg(_pubPackageFlag);
if (_targetPubPackage != null) {
final String? version = getNullableStringArg(_versionFlag);
if (version == null) {
final PubVersionFinderResponse response = await _pubVersionFinder
.getPackageVersion(packageName: _targetPubPackage!);
switch (response.result) {
case PubVersionFinderResult.success:
_targetVersion = response.versions.first.toString();
break;
case PubVersionFinderResult.fail:
printError('''
Error fetching $_targetPubPackage version from pub: ${response.httpResponse.statusCode}:
${response.httpResponse.body}
''');
throw ToolExit(_exitNoTargetVersion);
case PubVersionFinderResult.noPackageFound:
printError('$_targetPubPackage does not exist on pub');
throw ToolExit(_exitNoTargetVersion);
}
} else {
_targetVersion = version;
}
}
}
@override
Future<void> completeRun() async {
_pubVersionFinder.httpClient.close();
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
if (_targetPubPackage != null) {
return _runForPubDependency(package, _targetPubPackage!);
}
// TODO(stuartmorgan): Add othe dependency types here (e.g., maven).
return PackageResult.fail();
}
/// Handles all of the updates for [package] when the target dependency is
/// a pub dependency.
Future<PackageResult> _runForPubDependency(
RepositoryPackage package, String dependency) async {
final _PubDependencyInfo? dependencyInfo =
_getPubDependencyInfo(package, dependency);
if (dependencyInfo == null) {
return PackageResult.skip('Does not depend on $dependency');
} else if (!dependencyInfo.hosted) {
return PackageResult.skip('$dependency in not a hosted dependency');
}
final String sectionKey = dependencyInfo.type == _PubDependencyType.dev
? 'dev_dependencies'
: 'dependencies';
final String versionString;
final VersionConstraint parsedConstraint =
VersionConstraint.parse(_targetVersion);
// If the provided string was a constraint, or if it's a specific
// version but the package has a pinned dependency, use it as-is.
if (dependencyInfo.pinned ||
parsedConstraint is! VersionRange ||
parsedConstraint.min != parsedConstraint.max) {
versionString = _targetVersion;
} else {
// Otherwise, it's a specific version; treat it as '^version'.
final Version minVersion = parsedConstraint.min!;
versionString = '^$minVersion';
}
print('${indentation}Updating to "$versionString"');
if (versionString == dependencyInfo.constraintString) {
return PackageResult.skip('Already depends on $versionString');
}
final YamlEditor editablePubspec =
YamlEditor(package.pubspecFile.readAsStringSync());
editablePubspec.update(
<String>[sectionKey, dependency],
versionString,
);
package.pubspecFile.writeAsStringSync(editablePubspec.toString());
// TODO(stuartmorgan): Add additionally handling of known packages that
// do file generation (mockito, pigeon, etc.).
return PackageResult.success();
}
/// Returns information about the current dependency of [package] on
/// the package named [dependencyName], or null if there is no dependency.
_PubDependencyInfo? _getPubDependencyInfo(
RepositoryPackage package, String dependencyName) {
final Pubspec pubspec = package.parsePubspec();
Dependency? dependency;
final _PubDependencyType type;
if (pubspec.dependencies.containsKey(dependencyName)) {
dependency = pubspec.dependencies[dependencyName];
type = _PubDependencyType.normal;
} else if (pubspec.devDependencies.containsKey(dependencyName)) {
dependency = pubspec.devDependencies[dependencyName];
type = _PubDependencyType.dev;
} else {
return null;
}
if (dependency != null && dependency is HostedDependency) {
final VersionConstraint version = dependency.version;
return _PubDependencyInfo(
type,
pinned: version is VersionRange && version.min == version.max,
hosted: true,
constraintString: version.toString(),
);
}
return _PubDependencyInfo(type, pinned: false, hosted: false);
}
}
class _PubDependencyInfo {
const _PubDependencyInfo(this.type,
{required this.pinned, required this.hosted, this.constraintString});
final _PubDependencyType type;
final bool pinned;
final bool hosted;
final String? constraintString;
}
enum _PubDependencyType { normal, dev }

View File

@ -0,0 +1,343 @@
// Copyright 2013 The Flutter Authors. 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:convert';
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/update_dependency_command.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';
import 'util.dart';
void main() {
FileSystem fileSystem;
late Directory packagesDir;
late CommandRunner<void> runner;
Future<http.Response> Function(http.Request request)? mockHttpResponse;
setUp(() {
fileSystem = MemoryFileSystem();
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
final UpdateDependencyCommand command = UpdateDependencyCommand(
packagesDir,
httpClient:
MockClient((http.Request request) => mockHttpResponse!(request)),
);
runner = CommandRunner<void>(
'update_dependency_command', 'Test for update-dependency command.');
runner.addCommand(command);
});
/// Adds a dummy 'dependencies:' entries for [dependency] to [package].
void addDependency(RepositoryPackage package, String dependency,
{String version = '^1.0.0'}) {
final List<String> lines = package.pubspecFile.readAsLinesSync();
final int dependenciesStartIndex = lines.indexOf('dependencies:');
assert(dependenciesStartIndex != -1);
lines.insert(dependenciesStartIndex + 1, ' $dependency: $version');
package.pubspecFile.writeAsStringSync(lines.join('\n'));
}
/// Adds a 'dev_dependencies:' section with an entry for [dependency] to
/// [package].
void addDevDependency(RepositoryPackage package, String dependency,
{String version = '^1.0.0'}) {
final String originalContent = package.pubspecFile.readAsStringSync();
package.pubspecFile.writeAsStringSync('''
$originalContent
dev_dependencies:
$dependency: $version
''');
}
test('throws if no target is provided', () async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['update-dependency'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Exactly one of the target flags must be provided:'),
]),
);
});
group('pub dependencies', () {
test('throws if no version is given for an unpublished target', () async {
mockHttpResponse = (http.Request request) async {
return http.Response('', 404);
};
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package'
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('target_package does not exist on pub'),
]),
);
});
test('skips if there is no dependency', () async {
createFakePackage('a_package', packagesDir, examples: <String>[]);
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: Does not depend on target_package'),
]),
);
});
test('skips if the dependency is already the target version', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package', version: '^1.5.0');
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: Already depends on ^1.5.0'),
]),
);
});
test('logs updates', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package');
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Updating to "^1.5.0"'),
]),
);
});
test('updates normal dependency', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '^1.5.0');
});
test('updates dev dependency', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDevDependency(package, 'target_package');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
final Dependency? dep =
package.parsePubspec().devDependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '^1.5.0');
});
test('updates dependency in example', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
final RepositoryPackage example = package.getExamples().first;
addDevDependency(example, 'target_package');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
final Dependency? dep =
example.parsePubspec().devDependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '^1.5.0');
});
test('uses provided constraint as-is', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package');
const String providedConstraint = '>=1.6.0 <3.0.0';
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
providedConstraint
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), providedConstraint);
});
test('uses provided version as lower bound for unpinned', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '^1.5.0');
});
test('uses provided version as exact version for pinned', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package', version: '1.0.0');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
'--version',
'1.5.0'
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '1.5.0');
});
test('uses latest pub version as lower bound for unpinned', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package');
const Map<String, dynamic> targetPackagePubResponse = <String, dynamic>{
'name': 'a',
'versions': <String>[
'0.0.1',
'1.0.0',
'1.5.0',
],
};
mockHttpResponse = (http.Request request) async {
if (request.url.pathSegments.last == 'target_package.json') {
return http.Response(json.encode(targetPackagePubResponse), 200);
}
return http.Response('', 500);
};
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '^1.5.0');
});
test('uses latest pub version as exact version for pinned', () async {
final RepositoryPackage package =
createFakePackage('a_package', packagesDir, examples: <String>[]);
addDependency(package, 'target_package', version: '1.0.0');
const Map<String, dynamic> targetPackagePubResponse = <String, dynamic>{
'name': 'a',
'versions': <String>[
'0.0.1',
'1.0.0',
'1.5.0',
],
};
mockHttpResponse = (http.Request request) async {
if (request.url.pathSegments.last == 'target_package.json') {
return http.Response(json.encode(targetPackagePubResponse), 200);
}
return http.Response('', 500);
};
await runCapturingPrint(runner, <String>[
'update-dependency',
'--pub-package',
'target_package',
]);
final Dependency? dep =
package.parsePubspec().dependencies['target_package'];
expect(dep, isA<HostedDependency>());
expect((dep! as HostedDependency).version.toString(), '1.5.0');
});
});
}