Skip to content

Commit e006571

Browse files
stuartmorgan-gfotiDim
authored andcommitted
[flutter_plugin_tools] Support format on Windows (flutter#4150)
Allows `format` to run successfully on Windows: - Ensures that no calls exceed the command length limit. - Allows specifying a `java` path to make it easier to run without a system Java (e.g., by pointing to the `java` binary in an Android Studio installation). - Adds clear error messages when `java` or `clang-format` is missing since it's very non-obvious what's wrong otherwise. Bumps the version, which I intended to do in the previous PR but apparently didn't push to the PR.
1 parent fa9ba31 commit e006571

File tree

4 files changed

+383
-44
lines changed

4 files changed

+383
-44
lines changed

script/tool/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## NEXT
1+
## 0.4.0
22

33
- Modified the output format of many commands
44
- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart`
@@ -10,6 +10,7 @@
1010
- Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to
1111
work for now, but will be removed in the future.
1212
- Make `drive-examples` device detection robust against Flutter tool banners.
13+
- `format` is now supported on Windows.
1314

1415
## 0.3.0
1516

script/tool/lib/src/format_command.dart

Lines changed: 123 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,31 @@ import 'dart:io' as io;
77

88
import 'package:file/file.dart';
99
import 'package:http/http.dart' as http;
10+
import 'package:meta/meta.dart';
1011
import 'package:platform/platform.dart';
11-
import 'package:quiver/iterables.dart';
1212

1313
import 'common/core.dart';
1414
import 'common/plugin_command.dart';
1515
import 'common/process_runner.dart';
1616

17+
/// In theory this should be 8191, but in practice that was still resulting in
18+
/// "The input line is too long" errors. This was chosen as a value that worked
19+
/// in practice in testing with flutter/plugins, but may need to be adjusted
20+
/// based on further experience.
21+
@visibleForTesting
22+
const int windowsCommandLineMax = 8000;
23+
24+
/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a
25+
/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it
26+
/// can be lowered accordingly.
27+
@visibleForTesting
28+
const int nonWindowsCommandLineMax = 1000000;
29+
1730
const int _exitClangFormatFailed = 3;
1831
const int _exitFlutterFormatFailed = 4;
1932
const int _exitJavaFormatFailed = 5;
2033
const int _exitGitFailed = 6;
34+
const int _exitDependencyMissing = 7;
2135

2236
final Uri _googleFormatterUrl = Uri.https('github.com',
2337
'/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar');
@@ -32,8 +46,9 @@ class FormatCommand extends PluginCommand {
3246
}) : super(packagesDir, processRunner: processRunner, platform: platform) {
3347
argParser.addFlag('fail-on-change', hide: true);
3448
argParser.addOption('clang-format',
35-
defaultsTo: 'clang-format',
36-
help: 'Path to executable of clang-format.');
49+
defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.');
50+
argParser.addOption('java',
51+
defaultsTo: 'java', help: 'Path to "java" executable.');
3752
}
3853

3954
@override
@@ -52,7 +67,8 @@ class FormatCommand extends PluginCommand {
5267
// This class is not based on PackageLoopingCommand because running the
5368
// formatters separately for each package is an order of magnitude slower,
5469
// due to the startup overhead of the formatters.
55-
final Iterable<String> files = await _getFilteredFilePaths(getFiles());
70+
final Iterable<String> files =
71+
await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir);
5672
await _formatDart(files);
5773
await _formatJava(files, googleFormatterPath);
5874
await _formatCppAndObjectiveC(files);
@@ -112,19 +128,18 @@ class FormatCommand extends PluginCommand {
112128
final Iterable<String> clangFiles = _getPathsWithExtensions(
113129
files, <String>{'.h', '.m', '.mm', '.cc', '.cpp'});
114130
if (clangFiles.isNotEmpty) {
115-
print('Formatting .cc, .cpp, .h, .m, and .mm files...');
116-
final Iterable<List<String>> batches = partition(clangFiles, 100);
117-
int exitCode = 0;
118-
for (final List<String> batch in batches) {
119-
batch.sort(); // For ease of testing; partition changes the order.
120-
exitCode = await processRunner.runAndStream(
121-
getStringArg('clang-format'),
122-
<String>['-i', '--style=Google', ...batch],
123-
workingDir: packagesDir);
124-
if (exitCode != 0) {
125-
break;
126-
}
131+
final String clangFormat = getStringArg('clang-format');
132+
if (!await _hasDependency(clangFormat)) {
133+
printError(
134+
'Unable to run \'clang-format\'. Make sure that it is in your '
135+
'path, or provide a full path with --clang-format.');
136+
throw ToolExit(_exitDependencyMissing);
127137
}
138+
139+
print('Formatting .cc, .cpp, .h, .m, and .mm files...');
140+
final int exitCode = await _runBatched(
141+
getStringArg('clang-format'), <String>['-i', '--style=Google'],
142+
files: clangFiles);
128143
if (exitCode != 0) {
129144
printError(
130145
'Failed to format C, C++, and Objective-C files: exit code $exitCode.');
@@ -138,10 +153,18 @@ class FormatCommand extends PluginCommand {
138153
final Iterable<String> javaFiles =
139154
_getPathsWithExtensions(files, <String>{'.java'});
140155
if (javaFiles.isNotEmpty) {
156+
final String java = getStringArg('java');
157+
if (!await _hasDependency(java)) {
158+
printError(
159+
'Unable to run \'java\'. Make sure that it is in your path, or '
160+
'provide a full path with --java.');
161+
throw ToolExit(_exitDependencyMissing);
162+
}
163+
141164
print('Formatting .java files...');
142-
final int exitCode = await processRunner.runAndStream('java',
143-
<String>['-jar', googleFormatterPath, '--replace', ...javaFiles],
144-
workingDir: packagesDir);
165+
final int exitCode = await _runBatched(
166+
java, <String>['-jar', googleFormatterPath, '--replace'],
167+
files: javaFiles);
145168
if (exitCode != 0) {
146169
printError('Failed to format Java files: exit code $exitCode.');
147170
throw ToolExit(_exitJavaFormatFailed);
@@ -156,17 +179,21 @@ class FormatCommand extends PluginCommand {
156179
print('Formatting .dart files...');
157180
// `flutter format` doesn't require the project to actually be a Flutter
158181
// project.
159-
final int exitCode = await processRunner.runAndStream(
160-
flutterCommand, <String>['format', ...dartFiles],
161-
workingDir: packagesDir);
182+
final int exitCode = await _runBatched(flutterCommand, <String>['format'],
183+
files: dartFiles);
162184
if (exitCode != 0) {
163185
printError('Failed to format Dart files: exit code $exitCode.');
164186
throw ToolExit(_exitFlutterFormatFailed);
165187
}
166188
}
167189
}
168190

169-
Future<Iterable<String>> _getFilteredFilePaths(Stream<File> files) async {
191+
/// Given a stream of [files], returns the paths of any that are not in known
192+
/// locations to ignore, relative to [relativeTo].
193+
Future<Iterable<String>> _getFilteredFilePaths(
194+
Stream<File> files, {
195+
required Directory relativeTo,
196+
}) async {
170197
// Returns a pattern to check for [directories] as a subset of a file path.
171198
RegExp pathFragmentForDirectories(List<String> directories) {
172199
String s = path.separator;
@@ -177,8 +204,10 @@ class FormatCommand extends PluginCommand {
177204
return RegExp('(?:^|$s)${path.joinAll(directories)}$s');
178205
}
179206

207+
final String fromPath = relativeTo.path;
208+
180209
return files
181-
.map((File file) => file.path)
210+
.map((File file) => path.relative(file.path, from: fromPath))
182211
.where((String path) =>
183212
// Ignore files in build/ directories (e.g., headers of frameworks)
184213
// to avoid useless extra work in local repositories.
@@ -212,4 +241,74 @@ class FormatCommand extends PluginCommand {
212241

213242
return javaFormatterPath;
214243
}
244+
245+
/// Returns true if [command] can be run successfully.
246+
Future<bool> _hasDependency(String command) async {
247+
try {
248+
final io.ProcessResult result =
249+
await processRunner.run(command, <String>['--version']);
250+
if (result.exitCode != 0) {
251+
return false;
252+
}
253+
} on io.ProcessException {
254+
// Thrown when the binary is missing entirely.
255+
return false;
256+
}
257+
return true;
258+
}
259+
260+
/// Runs [command] on [arguments] on all of the files in [files], batched as
261+
/// necessary to avoid OS command-line length limits.
262+
///
263+
/// Returns the exit code of the first failure, which stops the run, or 0
264+
/// on success.
265+
Future<int> _runBatched(
266+
String command,
267+
List<String> arguments, {
268+
required Iterable<String> files,
269+
}) async {
270+
final int commandLineMax =
271+
platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax;
272+
273+
// Compute the max length of the file argument portion of a batch.
274+
// Add one to each argument's length for the space before it.
275+
final int argumentTotalLength =
276+
arguments.fold(0, (int sum, String arg) => sum + arg.length + 1);
277+
final int batchMaxTotalLength =
278+
commandLineMax - command.length - argumentTotalLength;
279+
280+
// Run the command in batches.
281+
final List<List<String>> batches =
282+
_partitionFileList(files, maxStringLength: batchMaxTotalLength);
283+
for (final List<String> batch in batches) {
284+
batch.sort(); // For ease of testing.
285+
final int exitCode = await processRunner.runAndStream(
286+
command, <String>[...arguments, ...batch],
287+
workingDir: packagesDir);
288+
if (exitCode != 0) {
289+
return exitCode;
290+
}
291+
}
292+
return 0;
293+
}
294+
295+
/// Partitions [files] into batches whose max string length as parameters to
296+
/// a command (including the spaces between them, and between the list and
297+
/// the command itself) is no longer than [maxStringLength].
298+
List<List<String>> _partitionFileList(Iterable<String> files,
299+
{required int maxStringLength}) {
300+
final List<List<String>> batches = <List<String>>[<String>[]];
301+
int currentBatchTotalLength = 0;
302+
for (final String file in files) {
303+
final int length = file.length + 1 /* for the space */;
304+
if (currentBatchTotalLength + length > maxStringLength) {
305+
// Start a new batch.
306+
batches.add(<String>[]);
307+
currentBatchTotalLength = 0;
308+
}
309+
batches.last.add(file);
310+
currentBatchTotalLength += length;
311+
}
312+
return batches;
313+
}
215314
}

script/tool/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: flutter_plugin_tools
22
description: Productivity utils for flutter/plugins and flutter/packages
33
repository: https://github.com/flutter/plugins/tree/master/script/tool
4-
version: 0.3.0
4+
version: 0.4.0
55

66
dependencies:
77
args: ^2.1.0

0 commit comments

Comments
 (0)