Unverified Commit 8c5a70f3 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

flutter update-packages --cherry-pick-package (#128917)

Fixes https://github.com/flutter/flutter/issues/101525
parent d3e771c8
......@@ -51,6 +51,14 @@ class UpdatePackagesCommand extends FlutterCommand {
'This will actually modify the pubspec.yaml files in your checkout.',
negatable: false,
)
..addOption(
'cherry-pick-package',
help: 'Attempt to update only the specified package. The "-cherry-pick-version" version must be specified also.',
)
..addOption(
'cherry-pick-version',
help: 'Attempt to update the package to the specified version. The "--cherry-pick-package" option must be specified also.',
)
..addFlag(
'paths',
help: 'Finds paths in the dependency chain leading from package specified '
......@@ -182,7 +190,8 @@ class UpdatePackagesCommand extends FlutterCommand {
final bool isVerifyOnly = boolArg('verify-only');
final bool isConsumerOnly = boolArg('consumer-only');
final bool offline = boolArg('offline');
final bool doUpgrade = forceUpgrade || isPrintPaths || isPrintTransitiveClosure;
final String? cherryPickPackage = stringArg('cherry-pick-package');
final String? cherryPickVersion = stringArg('cherry-pick-version');
if (boolArg('crash')) {
throw StateError('test crash please ignore.');
......@@ -194,6 +203,54 @@ class UpdatePackagesCommand extends FlutterCommand {
);
}
if (forceUpgrade && cherryPickPackage != null) {
throwToolExit(
'--force-upgrade cannot be used with the --cherry-pick-package flag'
);
}
if (forceUpgrade && isPrintPaths) {
throwToolExit(
'--force-upgrade cannot be used with the --paths flag'
);
}
if (forceUpgrade && isPrintTransitiveClosure) {
throwToolExit(
'--force-upgrade cannot be used with the --transitive-closure flag'
);
}
if (cherryPickPackage != null && offline) {
throwToolExit(
'--cherry-pick-package cannot be used with the --offline flag'
);
}
if (cherryPickPackage != null && cherryPickVersion == null) {
throwToolExit(
'--cherry-pick-version is required when using --cherry-pick-package flag'
);
}
if (isPrintPaths && (stringArg('from') == null || stringArg('to') == null)) {
throwToolExit(
'The --from and --to flags are required when using the --paths flag'
);
}
if (!isPrintPaths && (stringArg('from') != null || stringArg('to') != null)) {
throwToolExit(
'The --from and --to flags are only allowed when using the --paths flag'
);
}
if (isPrintTransitiveClosure && isPrintPaths) {
throwToolExit(
'The --transitive-closure flag cannot be used with the --paths flag'
);
}
// "consumer" packages are those that constitute our public API (e.g. flutter, flutter_test, flutter_driver, flutter_localizations, integration_test).
if (isConsumerOnly) {
if (!isPrintTransitiveClosure) {
......@@ -216,7 +273,7 @@ class UpdatePackagesCommand extends FlutterCommand {
return FlutterCommandResult.success();
}
if (doUpgrade) {
if (forceUpgrade) {
// This feature attempts to collect all the packages used across all the
// pubspec.yamls in the repo (including via transitive dependencies), and
// find the latest version of each that can be used while keeping each
......@@ -235,9 +292,38 @@ class UpdatePackagesCommand extends FlutterCommand {
explicitDependencies: explicitDependencies,
allDependencies: allDependencies,
specialDependencies: specialDependencies,
doUpgrade: doUpgrade,
printPaths: forceUpgrade || isPrintPaths || isPrintTransitiveClosure || cherryPickPackage != null,
);
final Iterable<PubspecDependency> baseDependencies;
if (cherryPickPackage != null) {
if (!allDependencies.containsKey(cherryPickPackage)) {
throwToolExit(
'Package "$cherryPickPackage" is not currently a dependency, and therefore cannot be upgraded.'
);
}
if (cherryPickVersion != null) {
globals.printStatus('Pinning package "$cherryPickPackage" to version "$cherryPickVersion"...');
} else {
globals.printStatus('Upgrading package "$cherryPickPackage"...');
}
final List<PubspecDependency> adjustedDependencies = <PubspecDependency>[];
for (final String package in allDependencies.keys) {
if (package == cherryPickPackage) {
assert(cherryPickVersion != null);
final PubspecDependency pubspec = allDependencies[cherryPickPackage]!;
adjustedDependencies.add(pubspec.copyWith(version: cherryPickVersion));
} else {
adjustedDependencies.add(allDependencies[package]!);
}
}
baseDependencies = adjustedDependencies;
} else if (forceUpgrade) {
baseDependencies = explicitDependencies.values;
} else {
baseDependencies = allDependencies.values;
}
// Now that we have all the dependencies we care about, we are going to
// create a fake package and then run either "pub upgrade", if requested,
// followed by "pub get" on it. If upgrading, the pub tool will attempt to
......@@ -246,12 +332,14 @@ class UpdatePackagesCommand extends FlutterCommand {
// attempt to download any necessary package versions to the pub cache to
// warm the cache.
final PubDependencyTree tree = PubDependencyTree(); // object to collect results
await _generateFakePackage(
await _pubGetAllDependencies(
tempDir: _syntheticPackageDir,
dependencies: doUpgrade ? explicitDependencies.values : allDependencies.values,
dependencies: baseDependencies,
pubspecs: pubspecs,
tree: tree,
doUpgrade: doUpgrade,
doUpgrade: forceUpgrade,
isolateEnvironment: forceUpgrade || isPrintPaths || isPrintTransitiveClosure || cherryPickPackage != null,
reportDependenciesToTree: forceUpgrade || isPrintPaths || isPrintTransitiveClosure || cherryPickPackage != null,
);
// Only delete the synthetic package if it was done in a temp directory
......@@ -259,18 +347,31 @@ class UpdatePackagesCommand extends FlutterCommand {
_syntheticPackageDir.deleteSync(recursive: true);
}
if (doUpgrade) {
final bool done = _upgradePubspecs(
if (forceUpgrade || isPrintTransitiveClosure || isPrintPaths || cherryPickPackage != null) {
_processPubspecs(
tree: tree,
pubspecs: pubspecs,
explicitDependencies: explicitDependencies,
specialDependencies: specialDependencies,
);
if (done) {
// Complete early if we were just printing data.
if (isPrintTransitiveClosure) {
tree._dependencyTree.forEach((String from, Set<String> to) {
globals.printStatus('$from -> $to');
});
return FlutterCommandResult.success();
}
if (isPrintPaths) {
showDependencyPaths(from: stringArg('from')!, to: stringArg('to')!, tree: tree);
return FlutterCommandResult.success();
}
globals.printStatus('Updating workspace...');
_updatePubspecs(
tree: tree,
pubspecs: pubspecs,
specialDependencies: specialDependencies,
);
}
await _runPubGetOnPackages(packages);
......@@ -306,8 +407,9 @@ class UpdatePackagesCommand extends FlutterCommand {
// we need to run update-packages to recapture the transitive deps.
globals.printWarning(
'Warning: pubspec in ${directory.path} has updated or new dependencies. '
'Please run "flutter update-packages --force-upgrade" to update them correctly '
'(checksum ${pubspec.checksum.value} != $checksum).'
'Please run "flutter update-packages --force-upgrade" to update them correctly.'
// DO NOT PRINT THE CHECKSUM HERE.
// It causes people to ignore the requirement to actually run the script.
);
needsUpdate = true;
} else {
......@@ -331,11 +433,11 @@ class UpdatePackagesCommand extends FlutterCommand {
required Set<String> specialDependencies,
required Map<String, PubspecDependency> explicitDependencies,
required Map<String, PubspecDependency> allDependencies,
required bool doUpgrade,
required bool printPaths,
}) {
// Visit all the directories with pubspec.yamls we care about.
for (final Directory directory in packages) {
if (doUpgrade) {
if (printPaths) {
globals.printTrace('Reading pubspec.yaml from: ${directory.path}');
}
final PubspecYaml pubspec = PubspecYaml(directory); // this parses the pubspec.yaml
......@@ -404,12 +506,14 @@ class UpdatePackagesCommand extends FlutterCommand {
}
}
Future<void> _generateFakePackage({
Future<void> _pubGetAllDependencies({
required Directory tempDir,
required Iterable<PubspecDependency> dependencies,
required List<PubspecYaml> pubspecs,
required PubDependencyTree tree,
required bool doUpgrade,
required bool isolateEnvironment,
required bool reportDependenciesToTree,
}) async {
Directory? temporaryFlutterSdk;
final Directory syntheticPackageDir = tempDir.childDirectory('synthetic_package');
......@@ -421,9 +525,10 @@ class UpdatePackagesCommand extends FlutterCommand {
doUpgrade: doUpgrade,
),
);
// Create a synthetic flutter SDK so that transitive flutter SDK
// constraints are not affected by this upgrade.
if (doUpgrade) {
if (isolateEnvironment) {
// Create a synthetic flutter SDK so that transitive flutter SDK
// constraints are not affected by this upgrade.
temporaryFlutterSdk = createTemporaryFlutterSdk(
globals.logger,
globals.fs,
......@@ -433,8 +538,10 @@ class UpdatePackagesCommand extends FlutterCommand {
);
}
// Next we run "pub get" on it in order to force the download of any
// Run "pub get" on it in order to force the download of any
// needed packages to the pub cache, upgrading if requested.
// TODO(ianh): If this fails, the tool exits silently.
// It can fail, e.g., if --cherry-pick-version is invalid.
await pub.get(
context: PubContext.updatePackages,
project: FlutterProject.fromDirectory(syntheticPackageDir),
......@@ -444,9 +551,9 @@ class UpdatePackagesCommand extends FlutterCommand {
outputMode: PubOutputMode.none,
);
if (doUpgrade) {
// If upgrading, we run "pub deps --style=compact" on the result. We
// pipe all the output to tree.fill(), which parses it so that it can
if (reportDependenciesToTree) {
// Run "pub deps --style=compact" on the result.
// We pipe all the output to tree.fill(), which parses it so that it can
// create a graph of all the dependencies so that we can figure out the
// transitive dependencies later. It also remembers which version was
// selected for each package.
......@@ -459,40 +566,29 @@ class UpdatePackagesCommand extends FlutterCommand {
}
}
bool _upgradePubspecs({
void _processPubspecs({
required PubDependencyTree tree,
required List<PubspecYaml> pubspecs,
required Set<String> specialDependencies,
required Map<String, PubspecDependency> explicitDependencies,
}) {
// The transitive dependency tree for the fake package does not contain
// dependencies between Flutter SDK packages and pub packages. We add them
// here.
for (final PubspecYaml pubspec in pubspecs) {
final String package = pubspec.name;
specialDependencies.add(package);
tree._versions[package] = pubspec.version;
assert(!tree._dependencyTree.containsKey(package));
tree._dependencyTree[package] = <String>{};
for (final PubspecDependency dependency in pubspec.dependencies) {
if (dependency.kind == DependencyKind.normal) {
tree._dependencyTree[package] ??= <String>{};
tree._dependencyTree[package]!.add(dependency.name);
}
}
}
}
if (boolArg('transitive-closure')) {
tree._dependencyTree.forEach((String from, Set<String> to) {
globals.printStatus('$from -> $to');
});
return true;
}
if (boolArg('paths')) {
showDependencyPaths(from: stringArg('from')!, to: stringArg('to')!, tree: tree);
return true;
}
bool _updatePubspecs({
required PubDependencyTree tree,
required List<PubspecYaml> pubspecs,
required Set<String> specialDependencies,
}) {
// Now that we have collected all the data, we can apply our dependency
// versions to each pubspec.yaml that we collected. This mutates the
// pubspec.yaml files.
......@@ -662,7 +758,6 @@ enum DependencyKind {
/// pubspec.yaml file.
const String kTransitiveMagicString= '# THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"';
/// This is the string output before a checksum of the packages used.
const String kDependencyChecksum = '# PUBSPEC CHECKSUM: ';
......@@ -1288,6 +1383,28 @@ class PubspecDependency extends PubspecLine {
static const String _sdkPrefix = ' sdk: ';
static const String _gitPrefix = ' git:';
PubspecDependency copyWith({
String? line,
String? name,
String? suffix,
bool? isTransitive,
DependencyKind? kind,
String? version,
String? sourcePath,
bool? isDevDependency,
}) {
return PubspecDependency(
line ?? this.line,
name ?? this.name,
suffix ?? this.suffix,
isTransitive: isTransitive ?? this.isTransitive,
kind: kind ?? this.kind,
version: version ?? this.version,
sourcePath: sourcePath ?? this.sourcePath,
isDevDependency: isDevDependency ?? this.isDevDependency,
);
}
/// Whether the dependency points to a package in the Flutter SDK.
///
/// There are two ways one can point to a Flutter package:
......@@ -1353,16 +1470,16 @@ class PubspecDependency extends PubspecLine {
/// This generates the entry for this dependency for the pubspec.yaml for the
/// fake package that we'll use to get the version numbers figured out.
///
/// When called with [doUpgrade] as [true], the version constrains will be set
/// to >= whatever the previous version was. If [doUpgrade] is [false], then
/// When called with [allowUpgrade] as [true], the version constrains will be set
/// to >= whatever the previous version was. If [allowUpgrade] is [false], then
/// the previous version is used again as an exact pin.
void describeForFakePubspec(StringBuffer dependencies, StringBuffer overrides, { bool doUpgrade = true }) {
void describeForFakePubspec(StringBuffer dependencies, StringBuffer overrides, { bool allowUpgrade = true }) {
final String versionToUse;
// This should only happen when manually adding new dependencies; otherwise
// versions should always be pinned exactly
if (version.isEmpty || version == 'any') {
versionToUse = 'any';
} else if (doUpgrade) {
} else if (allowUpgrade) {
// Must wrap in quotes for Yaml parsing
versionToUse = "'>= $version'";
} else {
......@@ -1445,7 +1562,7 @@ String generateFakePubspec(
// Don't add pinned dependency if it is not in the set of all transitive dependencies.
if (!allTransitive.contains(package)) {
if (verbose) {
globals.printStatus('Skipping $package because it was not transitive');
globals.printStatus(' - $package: $version (skipped because it was not a transitive dependency)');
}
return;
}
......@@ -1457,7 +1574,7 @@ String generateFakePubspec(
}
for (final PubspecDependency dependency in dependencies) {
if (!dependency.pointsToSdk) {
dependency.describeForFakePubspec(result, overrides, doUpgrade: doUpgrade);
dependency.describeForFakePubspec(result, overrides, allowUpgrade: doUpgrade);
}
}
result.write(overrides.toString());
......
......@@ -197,6 +197,73 @@ void main() {
Logger: () => logger,
});
testUsingContext('--cherry-pick-package', () async {
final UpdatePackagesCommand command = UpdatePackagesCommand();
await createTestCommandRunner(command).run(<String>[
'update-packages',
'--cherry-pick-package=vector_math',
'--cherry-pick-version=2.0.9',
]);
expect(pub.pubGetDirectories, equals(<String>[
'/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
'/flutter/examples',
'/flutter/packages/flutter',
]));
expect(pub.pubBatchDirectories, equals(<String>[
'/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
]));
expect(pub.pubspecYamls, hasLength(3));
final String output = pub.pubspecYamls.first;
expect(output, isNotNull);
expect(output, contains('collection: 1.14.11\n'));
expect(output, contains('meta: 1.1.8\n'));
expect(output, contains('typed_data: 1.1.6\n'));
expect(output, contains('vector_math: 2.0.9\n'));
expect(output, isNot(contains('vector_math: 2.0.8')));
expect(output, isNot(contains('vector_math: ">= 2.0.8"')));
expect(output, isNot(contains("vector_math: '>= 2.0.8'")));
}, overrides: <Type, Generator>{
Pub: () => pub,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Cache: () => Cache.test(
processManager: processManager,
),
Logger: () => logger,
});
testUsingContext('--force-upgrade', () async {
final UpdatePackagesCommand command = UpdatePackagesCommand();
await createTestCommandRunner(command).run(<String>[
'update-packages',
'--force-upgrade',
]);
expect(pub.pubGetDirectories, equals(<String>[
'/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
'/flutter/examples',
'/flutter/packages/flutter',
]));
expect(pub.pubBatchDirectories, equals(<String>[
'/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
]));
expect(pub.pubspecYamls, hasLength(3));
final String output = pub.pubspecYamls.first;
expect(output, isNotNull);
expect(output, contains("collection: '>= 1.14.11'\n"));
expect(output, contains("meta: '>= 1.1.8'\n"));
expect(output, contains("typed_data: '>= 1.1.6'\n"));
expect(output, contains("vector_math: '>= 2.0.8'\n"));
expect(output, isNot(contains('vector_math: 2.0.8')));
}, overrides: <Type, Generator>{
Pub: () => pub,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Cache: () => Cache.test(
processManager: processManager,
),
Logger: () => logger,
});
testUsingContext('force updates packages --synthetic-package-path', () async {
final UpdatePackagesCommand command = UpdatePackagesCommand();
const String dir = '/path/to/synthetic/package';
......@@ -272,6 +339,7 @@ class FakePub extends Fake implements Pub {
final FileSystem fileSystem;
final List<String> pubGetDirectories = <String>[];
final List<String> pubBatchDirectories = <String>[];
final List<String> pubspecYamls = <String>[];
@override
Future<void> get({
......@@ -287,6 +355,7 @@ class FakePub extends Fake implements Pub {
PubOutputMode outputMode = PubOutputMode.all,
}) async {
pubGetDirectories.add(project.directory.path);
pubspecYamls.add(project.directory.childFile('pubspec.yaml').readAsStringSync());
project.directory.childFile('pubspec.lock')
..createSync(recursive: true)
..writeAsStringSync('''
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment