mirror of
https://github.com/flutter/packages.git
synced 2025-08-06 17:28:42 +08:00

Updates links and references to `flutter/plugins` to use `flutter/packages` instead, including making the `repository` pubspec.yaml check stricter in the repo tooling to ensure all packages are pointing to the right place. Exceptions to the updates are: - Changelog entries. - Links to pull requests. This will re-publish all the moved plugins, thus fixing the current redness of `release` (due to the current versions not being tagged in this repository).
370 lines
14 KiB
Dart
370 lines
14 KiB
Dart
// 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 'dart:io' as io;
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:meta/meta.dart';
|
|
import 'package:platform/platform.dart';
|
|
|
|
import 'common/core.dart';
|
|
import 'common/package_command.dart';
|
|
import 'common/process_runner.dart';
|
|
|
|
/// In theory this should be 8191, but in practice that was still resulting in
|
|
/// "The input line is too long" errors. This was chosen as a value that worked
|
|
/// in practice via trial and error, but may need to be adjusted based on
|
|
/// further experience.
|
|
@visibleForTesting
|
|
const int windowsCommandLineMax = 8000;
|
|
|
|
/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a
|
|
/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it
|
|
/// can be lowered accordingly.
|
|
@visibleForTesting
|
|
const int nonWindowsCommandLineMax = 1000000;
|
|
|
|
const int _exitClangFormatFailed = 3;
|
|
const int _exitFlutterFormatFailed = 4;
|
|
const int _exitJavaFormatFailed = 5;
|
|
const int _exitGitFailed = 6;
|
|
const int _exitDependencyMissing = 7;
|
|
|
|
final Uri _googleFormatterUrl = Uri.https('github.com',
|
|
'/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar');
|
|
|
|
/// A command to format all package code.
|
|
class FormatCommand extends PackageCommand {
|
|
/// Creates an instance of the format command.
|
|
FormatCommand(
|
|
Directory packagesDir, {
|
|
ProcessRunner processRunner = const ProcessRunner(),
|
|
Platform platform = const LocalPlatform(),
|
|
}) : super(packagesDir, processRunner: processRunner, platform: platform) {
|
|
argParser.addFlag('fail-on-change', hide: true);
|
|
argParser.addOption('clang-format',
|
|
defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.');
|
|
argParser.addOption('java',
|
|
defaultsTo: 'java', help: 'Path to "java" executable.');
|
|
}
|
|
|
|
@override
|
|
final String name = 'format';
|
|
|
|
@override
|
|
final String description =
|
|
'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n'
|
|
'This command requires "git", "flutter" and "clang-format" v5 to be in '
|
|
'your path.';
|
|
|
|
@override
|
|
Future<void> run() async {
|
|
final String googleFormatterPath = await _getGoogleFormatterPath();
|
|
|
|
// This class is not based on PackageLoopingCommand because running the
|
|
// formatters separately for each package is an order of magnitude slower,
|
|
// due to the startup overhead of the formatters.
|
|
final Iterable<String> files =
|
|
await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir);
|
|
await _formatDart(files);
|
|
await _formatJava(files, googleFormatterPath);
|
|
await _formatCppAndObjectiveC(files);
|
|
|
|
if (getBoolArg('fail-on-change')) {
|
|
final bool modified = await _didModifyAnything();
|
|
if (modified) {
|
|
throw ToolExit(exitCommandFoundErrors);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<bool> _didModifyAnything() async {
|
|
final io.ProcessResult modifiedFiles = await processRunner.run(
|
|
'git',
|
|
<String>['ls-files', '--modified'],
|
|
workingDir: packagesDir,
|
|
logOnError: true,
|
|
);
|
|
if (modifiedFiles.exitCode != 0) {
|
|
printError('Unable to determine changed files.');
|
|
throw ToolExit(_exitGitFailed);
|
|
}
|
|
|
|
print('\n\n');
|
|
|
|
final String stdout = modifiedFiles.stdout as String;
|
|
if (stdout.isEmpty) {
|
|
print('All files formatted correctly.');
|
|
return false;
|
|
}
|
|
|
|
print('These files are not formatted correctly (see diff below):');
|
|
LineSplitter.split(stdout).map((String line) => ' $line').forEach(print);
|
|
|
|
print('\nTo fix run "dart pub global activate flutter_plugin_tools && '
|
|
'dart pub global run flutter_plugin_tools format" or copy-paste '
|
|
'this command into your terminal:');
|
|
|
|
final io.ProcessResult diff = await processRunner.run(
|
|
'git',
|
|
<String>['diff'],
|
|
workingDir: packagesDir,
|
|
logOnError: true,
|
|
);
|
|
if (diff.exitCode != 0) {
|
|
printError('Unable to determine diff.');
|
|
throw ToolExit(_exitGitFailed);
|
|
}
|
|
print('patch -p1 <<DONE');
|
|
print(diff.stdout);
|
|
print('DONE');
|
|
return true;
|
|
}
|
|
|
|
Future<void> _formatCppAndObjectiveC(Iterable<String> files) async {
|
|
final Iterable<String> clangFiles = _getPathsWithExtensions(
|
|
files, <String>{'.h', '.m', '.mm', '.cc', '.cpp'});
|
|
if (clangFiles.isNotEmpty) {
|
|
final String clangFormat = await _findValidClangFormat();
|
|
|
|
print('Formatting .cc, .cpp, .h, .m, and .mm files...');
|
|
final int exitCode = await _runBatched(
|
|
clangFormat, <String>['-i', '--style=file'],
|
|
files: clangFiles);
|
|
if (exitCode != 0) {
|
|
printError(
|
|
'Failed to format C, C++, and Objective-C files: exit code $exitCode.');
|
|
throw ToolExit(_exitClangFormatFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String> _findValidClangFormat() async {
|
|
final String clangFormatArg = getStringArg('clang-format');
|
|
if (await _hasDependency(clangFormatArg)) {
|
|
return clangFormatArg;
|
|
}
|
|
|
|
// There is a known issue where "chromium/depot_tools/clang-format"
|
|
// fails with "Problem while looking for clang-format in Chromium source tree".
|
|
// Loop through all "clang-format"s in PATH until a working one is found,
|
|
// for example "/usr/local/bin/clang-format" or a "brew" installed version.
|
|
for (final String clangFormatPath in await _whichAll('clang-format')) {
|
|
if (await _hasDependency(clangFormatPath)) {
|
|
return clangFormatPath;
|
|
}
|
|
}
|
|
printError('Unable to run "clang-format". Make sure that it is in your '
|
|
'path, or provide a full path with --clang-format.');
|
|
throw ToolExit(_exitDependencyMissing);
|
|
}
|
|
|
|
Future<void> _formatJava(
|
|
Iterable<String> files, String googleFormatterPath) async {
|
|
final Iterable<String> javaFiles =
|
|
_getPathsWithExtensions(files, <String>{'.java'});
|
|
if (javaFiles.isNotEmpty) {
|
|
final String java = getStringArg('java');
|
|
if (!await _hasDependency(java)) {
|
|
printError(
|
|
'Unable to run "java". Make sure that it is in your path, or '
|
|
'provide a full path with --java.');
|
|
throw ToolExit(_exitDependencyMissing);
|
|
}
|
|
|
|
print('Formatting .java files...');
|
|
final int exitCode = await _runBatched(
|
|
java, <String>['-jar', googleFormatterPath, '--replace'],
|
|
files: javaFiles);
|
|
if (exitCode != 0) {
|
|
printError('Failed to format Java files: exit code $exitCode.');
|
|
throw ToolExit(_exitJavaFormatFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _formatDart(Iterable<String> files) async {
|
|
final Iterable<String> dartFiles =
|
|
_getPathsWithExtensions(files, <String>{'.dart'});
|
|
if (dartFiles.isNotEmpty) {
|
|
print('Formatting .dart files...');
|
|
final int exitCode =
|
|
await _runBatched('dart', <String>['format'], files: dartFiles);
|
|
if (exitCode != 0) {
|
|
printError('Failed to format Dart files: exit code $exitCode.');
|
|
throw ToolExit(_exitFlutterFormatFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a stream of [files], returns the paths of any that are not in known
|
|
/// locations to ignore, relative to [relativeTo].
|
|
Future<Iterable<String>> _getFilteredFilePaths(
|
|
Stream<File> files, {
|
|
required Directory relativeTo,
|
|
}) async {
|
|
// Returns a pattern to check for [directories] as a subset of a file path.
|
|
RegExp pathFragmentForDirectories(List<String> directories) {
|
|
String s = path.separator;
|
|
// Escape the separator for use in the regex.
|
|
if (s == r'\') {
|
|
s = r'\\';
|
|
}
|
|
return RegExp('(?:^|$s)${path.joinAll(directories)}$s');
|
|
}
|
|
|
|
final String fromPath = relativeTo.path;
|
|
|
|
// Dart files are allowed to have a pragma to disable auto-formatting. This
|
|
// was added because Hixie hurts when dealing with what dartfmt does to
|
|
// artisanally-formatted Dart, while Stuart gets really frustrated when
|
|
// dealing with PRs from newer contributors who don't know how to make Dart
|
|
// readable. After much discussion, it was decided that files in the plugins
|
|
// and packages repos that really benefit from hand-formatting (e.g. files
|
|
// with large blobs of hex literals) could be opted-out of the requirement
|
|
// that they be autoformatted, so long as the code's owner was willing to
|
|
// bear the cost of this during code reviews.
|
|
// In the event that code ownership moves to someone who does not hold the
|
|
// same views as the original owner, the pragma can be removed and the file
|
|
// auto-formatted.
|
|
const String handFormattedExtension = '.dart';
|
|
const String handFormattedPragma = '// This file is hand-formatted.';
|
|
|
|
return files
|
|
.where((File file) {
|
|
// See comment above near [handFormattedPragma].
|
|
return path.extension(file.path) != handFormattedExtension ||
|
|
!file.readAsLinesSync().contains(handFormattedPragma);
|
|
})
|
|
.map((File file) => path.relative(file.path, from: fromPath))
|
|
.where((String path) =>
|
|
// Ignore files in build/ directories (e.g., headers of frameworks)
|
|
// to avoid useless extra work in local repositories.
|
|
!path.contains(
|
|
pathFragmentForDirectories(<String>['example', 'build'])) &&
|
|
// Ignore files in Pods, which are not part of the repository.
|
|
!path.contains(pathFragmentForDirectories(<String>['Pods'])) &&
|
|
// Ignore .dart_tool/, which can have various intermediate files.
|
|
!path.contains(pathFragmentForDirectories(<String>['.dart_tool'])))
|
|
.toList();
|
|
}
|
|
|
|
Iterable<String> _getPathsWithExtensions(
|
|
Iterable<String> files, Set<String> extensions) {
|
|
return files.where(
|
|
(String filePath) => extensions.contains(path.extension(filePath)));
|
|
}
|
|
|
|
Future<String> _getGoogleFormatterPath() async {
|
|
final String javaFormatterPath = path.join(
|
|
path.dirname(path.fromUri(platform.script)),
|
|
'google-java-format-1.3-all-deps.jar');
|
|
final File javaFormatterFile =
|
|
packagesDir.fileSystem.file(javaFormatterPath);
|
|
|
|
if (!javaFormatterFile.existsSync()) {
|
|
print('Downloading Google Java Format...');
|
|
final http.Response response = await http.get(_googleFormatterUrl);
|
|
javaFormatterFile.writeAsBytesSync(response.bodyBytes);
|
|
}
|
|
|
|
return javaFormatterPath;
|
|
}
|
|
|
|
/// Returns true if [command] can be run successfully.
|
|
Future<bool> _hasDependency(String command) async {
|
|
// Some versions of Java accept both -version and --version, but some only
|
|
// accept -version.
|
|
final String versionFlag = command == 'java' ? '-version' : '--version';
|
|
try {
|
|
final io.ProcessResult result =
|
|
await processRunner.run(command, <String>[versionFlag]);
|
|
if (result.exitCode != 0) {
|
|
return false;
|
|
}
|
|
} on io.ProcessException {
|
|
// Thrown when the binary is missing entirely.
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Returns all instances of [command] executable found on user path.
|
|
Future<List<String>> _whichAll(String command) async {
|
|
try {
|
|
final io.ProcessResult result =
|
|
await processRunner.run('which', <String>['-a', command]);
|
|
|
|
if (result.exitCode != 0) {
|
|
return <String>[];
|
|
}
|
|
|
|
final String stdout = (result.stdout as String).trim();
|
|
if (stdout.isEmpty) {
|
|
return <String>[];
|
|
}
|
|
return LineSplitter.split(stdout).toList();
|
|
} on io.ProcessException {
|
|
return <String>[];
|
|
}
|
|
}
|
|
|
|
/// Runs [command] on [arguments] on all of the files in [files], batched as
|
|
/// necessary to avoid OS command-line length limits.
|
|
///
|
|
/// Returns the exit code of the first failure, which stops the run, or 0
|
|
/// on success.
|
|
Future<int> _runBatched(
|
|
String command,
|
|
List<String> arguments, {
|
|
required Iterable<String> files,
|
|
}) async {
|
|
final int commandLineMax =
|
|
platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax;
|
|
|
|
// Compute the max length of the file argument portion of a batch.
|
|
// Add one to each argument's length for the space before it.
|
|
final int argumentTotalLength =
|
|
arguments.fold(0, (int sum, String arg) => sum + arg.length + 1);
|
|
final int batchMaxTotalLength =
|
|
commandLineMax - command.length - argumentTotalLength;
|
|
|
|
// Run the command in batches.
|
|
final List<List<String>> batches =
|
|
_partitionFileList(files, maxStringLength: batchMaxTotalLength);
|
|
for (final List<String> batch in batches) {
|
|
batch.sort(); // For ease of testing.
|
|
final int exitCode = await processRunner.runAndStream(
|
|
command, <String>[...arguments, ...batch],
|
|
workingDir: packagesDir);
|
|
if (exitCode != 0) {
|
|
return exitCode;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Partitions [files] into batches whose max string length as parameters to
|
|
/// a command (including the spaces between them, and between the list and
|
|
/// the command itself) is no longer than [maxStringLength].
|
|
List<List<String>> _partitionFileList(Iterable<String> files,
|
|
{required int maxStringLength}) {
|
|
final List<List<String>> batches = <List<String>>[<String>[]];
|
|
int currentBatchTotalLength = 0;
|
|
for (final String file in files) {
|
|
final int length = file.length + 1 /* for the space */;
|
|
if (currentBatchTotalLength + length > maxStringLength) {
|
|
// Start a new batch.
|
|
batches.add(<String>[]);
|
|
currentBatchTotalLength = 0;
|
|
}
|
|
batches.last.add(file);
|
|
currentBatchTotalLength += length;
|
|
}
|
|
return batches;
|
|
}
|
|
}
|