blob: cfe99313068e72014e0a219f7924f402fe36a1c4 [file] [log] [blame]
// 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:colorize/colorize.dart';
import 'package:file/file.dart';
import 'package:git/git.dart';
import 'package:path/path.dart' as p;
import 'core.dart';
import 'plugin_command.dart';
import 'process_runner.dart';
/// An abstract base class for a command that iterates over a set of packages
/// controlled by a standard set of flags, running some actions on each package,
/// and collecting and reporting the success/failure of those actions.
abstract class PackageLoopingCommand extends PluginCommand {
/// Creates a command to operate on [packagesDir] with the given environment.
PackageLoopingCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
GitDir? gitDir,
}) : super(packagesDir, processRunner: processRunner, gitDir: gitDir);
/// Called during [run] before any calls to [runForPackage]. This provides an
/// opportunity to fail early if the command can't be run (e.g., because the
/// arguments are invalid), and to set up any run-level state.
Future<void> initializeRun() async {}
/// Runs the command for [package], returning a list of errors.
///
/// Errors may either be an empty string if there is no context that should
/// be included in the final error summary (e.g., a command that only has a
/// single failure mode), or strings that should be listed for that package
/// in the final summary. An empty list indicates success.
Future<List<String>> runForPackage(Directory package);
/// Called during [run] after all calls to [runForPackage]. This provides an
/// opportunity to do any cleanup of run-level state.
Future<void> completeRun() async {}
/// Whether or not the output (if any) of [runForPackage] is long, or short.
///
/// This changes the logging that happens at the start of each package's
/// run; long output gets a banner-style message to make it easier to find,
/// while short output gets a single-line entry.
///
/// When this is false, runForPackage output should be indented if possible,
/// to make the output structure easier to follow.
bool get hasLongOutput => true;
/// Whether to loop over all packages (e.g., including example/), rather than
/// only top-level packages.
bool get includeSubpackages => false;
/// The text to output at the start when reporting one or more failures.
/// This will be followed by a list of packages that reported errors, with
/// the per-package details if any.
///
/// This only needs to be overridden if the summary should provide extra
/// context.
String get failureListHeader => 'The following packages had errors:';
/// The text to output at the end when reporting one or more failures. This
/// will be printed immediately after the a list of packages that reported
/// errors.
///
/// This only needs to be overridden if the summary should provide extra
/// context.
String get failureListFooter => 'See above for full details.';
// ----------------------------------------
/// A convenience constant for [runForPackage] success that's more
/// self-documenting than the value.
static const List<String> success = <String>[];
/// A convenience constant for [runForPackage] failure without additional
/// context that's more self-documenting than the value.
static const List<String> failure = <String>[''];
/// Prints a message using a standard format indicating that the package was
/// skipped, with an explanation of why.
void printSkip(String reason) {
print(Colorize('SKIPPING: $reason')..darkGray());
}
/// Returns the identifying name to use for [package].
///
/// Implementations should not expect a specific format for this string, since
/// it uses heuristics to try to be precise without being overly verbose. If
/// an exact format (e.g., published name, or basename) is required, that
/// should be used instead.
String getPackageDescription(Directory package) {
String packageName = p.relative(package.path, from: packagesDir.path);
final List<String> components = p.split(packageName);
// For the common federated plugin pattern of `foo/foo_subpackage`, drop
// the first part since it's not useful.
if (components.length == 2 &&
components[1].startsWith('${components[0]}_')) {
packageName = components[1];
}
return packageName;
}
/// The suggested indentation for printed output.
String get indentation => hasLongOutput ? '' : ' ';
// ----------------------------------------
@override
Future<void> run() async {
await initializeRun();
final List<Directory> packages = includeSubpackages
? await getPackages().toList()
: await getPlugins().toList();
final Map<Directory, List<String>> results = <Directory, List<String>>{};
for (final Directory package in packages) {
_printPackageHeading(package);
results[package] = await runForPackage(package);
}
completeRun();
// If there were any errors reported, summarize them and exit.
if (results.values.any((List<String> failures) => failures.isNotEmpty)) {
const String indentation = ' ';
printError(failureListHeader);
for (final Directory package in packages) {
final List<String> errors = results[package]!;
if (errors.isNotEmpty) {
final String errorIndentation = indentation * 2;
String errorDetails = errors.join('\n$errorIndentation');
if (errorDetails.isNotEmpty) {
errorDetails = ':\n$errorIndentation$errorDetails';
}
printError(
'$indentation${getPackageDescription(package)}$errorDetails');
}
}
printError(failureListFooter);
throw ToolExit(exitCommandFoundErrors);
}
printSuccess('\n\nNo issues found!');
}
/// Prints the status message indicating that the command is being run for
/// [package].
///
/// Something is always printed to make it easier to distinguish between
/// a command running for a package and producing no output, and a command
/// not having been run for a package.
void _printPackageHeading(Directory package) {
String heading = 'Running for ${getPackageDescription(package)}';
if (hasLongOutput) {
heading = '''
============================================================
|| $heading
============================================================
''';
} else {
heading = '$heading...';
}
print(Colorize(heading)..cyan());
}
}