mirror of
https://github.com/flutter/packages.git
synced 2025-06-14 09:14:30 +08:00
[flutter_plugin_tools] Add a new 'make-deps-path-based' command (#4575)
Adds a new command that adds `dependency_overrides` to any packages in the repository that depend on a list of target packages, including an option to target packages that will publish a non-breaking change in a given diff. Adds a new CI step that uses the above in conjunction with a new `--run-on-dirty-packages` to adjust the dependencies of anything in the repository that uses a to-be-published package and then re-run analysis on just those packages. This will allow us to catch in presubmit any changes that are not breaking from a semver standpoint, but will break us due to our strict analysis in CI. Fixes https://github.com/flutter/flutter/issues/89862
This commit is contained in:
249
script/tool/lib/src/make_deps_path_based_command.dart
Normal file
249
script/tool/lib/src/make_deps_path_based_command.dart
Normal file
@ -0,0 +1,249 @@
|
||||
// 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:flutter_plugin_tools/src/common/repository_package.dart';
|
||||
import 'package:git/git.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
|
||||
import 'common/core.dart';
|
||||
import 'common/git_version_finder.dart';
|
||||
import 'common/plugin_command.dart';
|
||||
|
||||
const int _exitPackageNotFound = 3;
|
||||
const int _exitCannotUpdatePubspec = 4;
|
||||
|
||||
/// Converts all dependencies on target packages to path-based dependencies.
|
||||
///
|
||||
/// This is to allow for pre-publish testing of changes that could affect other
|
||||
/// packages in the repository. For instance, this allows for catching cases
|
||||
/// where a non-breaking change to a platform interface package of a federated
|
||||
/// plugin would cause post-publish analyzer failures in another package of that
|
||||
/// plugin.
|
||||
class MakeDepsPathBasedCommand extends PluginCommand {
|
||||
/// Creates an instance of the command to convert selected dependencies to
|
||||
/// path-based.
|
||||
MakeDepsPathBasedCommand(
|
||||
Directory packagesDir, {
|
||||
GitDir? gitDir,
|
||||
}) : super(packagesDir, gitDir: gitDir) {
|
||||
argParser.addMultiOption(_targetDependenciesArg,
|
||||
help:
|
||||
'The names of the packages to convert to path-based dependencies.\n'
|
||||
'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is '
|
||||
'passed.',
|
||||
valueHelp: 'some_package');
|
||||
argParser.addFlag(
|
||||
_targetDependenciesWithNonBreakingUpdatesArg,
|
||||
help: 'Causes all packages that have non-breaking version changes '
|
||||
'when compared against the git base to be treated as target '
|
||||
'packages.',
|
||||
);
|
||||
}
|
||||
|
||||
static const String _targetDependenciesArg = 'target-dependencies';
|
||||
static const String _targetDependenciesWithNonBreakingUpdatesArg =
|
||||
'target-dependencies-with-non-breaking-updates';
|
||||
|
||||
@override
|
||||
final String name = 'make-deps-path-based';
|
||||
|
||||
@override
|
||||
final String description =
|
||||
'Converts package dependencies to path-based references.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final Set<String> targetDependencies =
|
||||
getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg)
|
||||
? await _getNonBreakingUpdatePackages()
|
||||
: getStringListArg(_targetDependenciesArg).toSet();
|
||||
|
||||
if (targetDependencies.isEmpty) {
|
||||
print('No target dependencies; nothing to do.');
|
||||
return;
|
||||
}
|
||||
print('Rewriting references to: ${targetDependencies.join(', ')}...');
|
||||
|
||||
final Map<String, RepositoryPackage> localDependencyPackages =
|
||||
_findLocalPackages(targetDependencies);
|
||||
|
||||
final String repoRootPath = (await gitDir).path;
|
||||
for (final File pubspec in await _getAllPubspecs()) {
|
||||
if (await _addDependencyOverridesIfNecessary(
|
||||
pubspec, localDependencyPackages)) {
|
||||
// Print the relative path of the changed pubspec.
|
||||
final String displayPath = p.posix.joinAll(path
|
||||
.split(path.relative(pubspec.absolute.path, from: repoRootPath)));
|
||||
print(' Modified $displayPath');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, RepositoryPackage> _findLocalPackages(Set<String> packageNames) {
|
||||
final Map<String, RepositoryPackage> targets =
|
||||
<String, RepositoryPackage>{};
|
||||
for (final String packageName in packageNames) {
|
||||
final Directory topLevelCandidate =
|
||||
packagesDir.childDirectory(packageName);
|
||||
// If packages/<packageName>/ exists, then either that directory is the
|
||||
// package, or packages/<packageName>/<packageName>/ exists and is the
|
||||
// package (in the case of a federated plugin).
|
||||
if (topLevelCandidate.existsSync()) {
|
||||
final Directory appFacingCandidate =
|
||||
topLevelCandidate.childDirectory(packageName);
|
||||
targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync()
|
||||
? appFacingCandidate
|
||||
: topLevelCandidate);
|
||||
continue;
|
||||
}
|
||||
// If there is no packages/<packageName> directory, then either the
|
||||
// packages doesn't exist, or it is a sub-package of a federated plugin.
|
||||
// If it's the latter, it will be a directory whose name is a prefix.
|
||||
for (final FileSystemEntity entity in packagesDir.listSync()) {
|
||||
if (entity is Directory && packageName.startsWith(entity.basename)) {
|
||||
final Directory subPackageCandidate =
|
||||
entity.childDirectory(packageName);
|
||||
if (subPackageCandidate.existsSync()) {
|
||||
targets[packageName] = RepositoryPackage(subPackageCandidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targets.containsKey(packageName)) {
|
||||
printError('Unable to find package "$packageName"');
|
||||
throw ToolExit(_exitPackageNotFound);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// If [pubspecFile] has any dependencies on packages in [localDependencies],
|
||||
/// adds dependency_overrides entries to redirect them to the local version
|
||||
/// using path-based dependencies.
|
||||
///
|
||||
/// Returns true if any changes were made.
|
||||
Future<bool> _addDependencyOverridesIfNecessary(File pubspecFile,
|
||||
Map<String, RepositoryPackage> localDependencies) async {
|
||||
final String pubspecContents = pubspecFile.readAsStringSync();
|
||||
final Pubspec pubspec = Pubspec.parse(pubspecContents);
|
||||
// Fail if there are any dependency overrides already. If support for that
|
||||
// is needed at some point, it can be added, but currently it's not and
|
||||
// relying on that makes the logic here much simpler.
|
||||
if (pubspec.dependencyOverrides.isNotEmpty) {
|
||||
printError(
|
||||
'Plugins with dependency overrides are not currently supported.');
|
||||
throw ToolExit(_exitCannotUpdatePubspec);
|
||||
}
|
||||
|
||||
final Iterable<String> packagesToOverride = pubspec.dependencies.keys.where(
|
||||
(String packageName) => localDependencies.containsKey(packageName));
|
||||
if (packagesToOverride.isNotEmpty) {
|
||||
final String commonBasePath = packagesDir.path;
|
||||
// Find the relative path to the common base.
|
||||
final int packageDepth = path
|
||||
.split(path.relative(pubspecFile.parent.absolute.path,
|
||||
from: commonBasePath))
|
||||
.length;
|
||||
final List<String> relativeBasePathComponents =
|
||||
List<String>.filled(packageDepth, '..');
|
||||
// This is done via strings rather than by manipulating the Pubspec and
|
||||
// then re-serialiazing so that it's a localized change, rather than
|
||||
// rewriting the whole file (e.g., destroying comments), which could be
|
||||
// more disruptive for local use.
|
||||
String newPubspecContents = pubspecContents +
|
||||
'''
|
||||
|
||||
# FOR TESTING ONLY. DO NOT MERGE.
|
||||
dependency_overrides:
|
||||
''';
|
||||
for (final String packageName in packagesToOverride) {
|
||||
// Find the relative path from the common base to the local package.
|
||||
final List<String> repoRelativePathComponents = path.split(
|
||||
path.relative(localDependencies[packageName]!.directory.path,
|
||||
from: commonBasePath));
|
||||
newPubspecContents += '''
|
||||
$packageName:
|
||||
path: ${p.posix.joinAll(<String>[
|
||||
...relativeBasePathComponents,
|
||||
...repoRelativePathComponents,
|
||||
])}
|
||||
''';
|
||||
}
|
||||
pubspecFile.writeAsStringSync(newPubspecContents);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns all pubspecs anywhere under the packages directory.
|
||||
Future<List<File>> _getAllPubspecs() => packagesDir.parent
|
||||
.list(recursive: true, followLinks: false)
|
||||
.where((FileSystemEntity entity) =>
|
||||
entity is File && p.basename(entity.path) == 'pubspec.yaml')
|
||||
.map((FileSystemEntity file) => file as File)
|
||||
.toList();
|
||||
|
||||
/// Returns all packages that have non-breaking published changes (i.e., a
|
||||
/// minor or bugfix version change) relative to the git comparison base.
|
||||
///
|
||||
/// Prints status information about what was checked for ease of auditing logs
|
||||
/// in CI.
|
||||
Future<Set<String>> _getNonBreakingUpdatePackages() async {
|
||||
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
||||
final String baseSha = await gitVersionFinder.getBaseSha();
|
||||
print('Finding changed packages relative to "$baseSha"\n');
|
||||
|
||||
final Set<String> changedPackages = <String>{};
|
||||
for (final String path in await gitVersionFinder.getChangedFiles()) {
|
||||
// Git output always uses Posix paths.
|
||||
final List<String> allComponents = p.posix.split(path);
|
||||
// Only pubspec changes are potential publishing events.
|
||||
if (allComponents.last != 'pubspec.yaml' ||
|
||||
allComponents.contains('example')) {
|
||||
continue;
|
||||
}
|
||||
final RepositoryPackage package =
|
||||
RepositoryPackage(packagesDir.fileSystem.file(path).parent);
|
||||
final String packageName = package.parsePubspec().name;
|
||||
if (!await _hasNonBreakingVersionChange(package)) {
|
||||
// Log packages that had pubspec changes but weren't included for ease
|
||||
// of auditing CI.
|
||||
print(' Skipping $packageName; no non-breaking version change.');
|
||||
continue;
|
||||
}
|
||||
changedPackages.add(packageName);
|
||||
}
|
||||
return changedPackages;
|
||||
}
|
||||
|
||||
Future<bool> _hasNonBreakingVersionChange(RepositoryPackage package) async {
|
||||
final Pubspec pubspec = package.parsePubspec();
|
||||
if (pubspec.publishTo == 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String pubspecGitPath = p.posix.joinAll(path.split(path.relative(
|
||||
package.pubspecFile.absolute.path,
|
||||
from: (await gitDir).path)));
|
||||
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
||||
final Version? previousVersion =
|
||||
await gitVersionFinder.getPackageVersion(pubspecGitPath);
|
||||
if (previousVersion == null) {
|
||||
// The plugin is new, so nothing can be depending on it yet.
|
||||
return false;
|
||||
}
|
||||
final Version newVersion = pubspec.version!;
|
||||
if ((newVersion.major > 0 && newVersion.major != previousVersion.major) ||
|
||||
(newVersion.major == 0 && newVersion.minor != previousVersion.minor)) {
|
||||
// Breaking changes aren't targetted since they won't be picked up
|
||||
// automatically.
|
||||
return false;
|
||||
}
|
||||
return newVersion != previousVersion;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user