@@ -7,17 +7,31 @@ import 'dart:io' as io;
7
7
8
8
import 'package:file/file.dart' ;
9
9
import 'package:http/http.dart' as http;
10
+ import 'package:meta/meta.dart' ;
10
11
import 'package:platform/platform.dart' ;
11
- import 'package:quiver/iterables.dart' ;
12
12
13
13
import 'common/core.dart' ;
14
14
import 'common/plugin_command.dart' ;
15
15
import 'common/process_runner.dart' ;
16
16
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
+
17
30
const int _exitClangFormatFailed = 3 ;
18
31
const int _exitFlutterFormatFailed = 4 ;
19
32
const int _exitJavaFormatFailed = 5 ;
20
33
const int _exitGitFailed = 6 ;
34
+ const int _exitDependencyMissing = 7 ;
21
35
22
36
final Uri _googleFormatterUrl = Uri .https ('github.com' ,
23
37
'/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 {
32
46
}) : super (packagesDir, processRunner: processRunner, platform: platform) {
33
47
argParser.addFlag ('fail-on-change' , hide: true );
34
48
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.' );
37
52
}
38
53
39
54
@override
@@ -52,7 +67,8 @@ class FormatCommand extends PluginCommand {
52
67
// This class is not based on PackageLoopingCommand because running the
53
68
// formatters separately for each package is an order of magnitude slower,
54
69
// 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);
56
72
await _formatDart (files);
57
73
await _formatJava (files, googleFormatterPath);
58
74
await _formatCppAndObjectiveC (files);
@@ -112,19 +128,18 @@ class FormatCommand extends PluginCommand {
112
128
final Iterable <String > clangFiles = _getPathsWithExtensions (
113
129
files, < String > {'.h' , '.m' , '.mm' , '.cc' , '.cpp' });
114
130
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);
127
137
}
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);
128
143
if (exitCode != 0 ) {
129
144
printError (
130
145
'Failed to format C, C++, and Objective-C files: exit code $exitCode .' );
@@ -138,10 +153,18 @@ class FormatCommand extends PluginCommand {
138
153
final Iterable <String > javaFiles =
139
154
_getPathsWithExtensions (files, < String > {'.java' });
140
155
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
+
141
164
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 );
145
168
if (exitCode != 0 ) {
146
169
printError ('Failed to format Java files: exit code $exitCode .' );
147
170
throw ToolExit (_exitJavaFormatFailed);
@@ -156,17 +179,21 @@ class FormatCommand extends PluginCommand {
156
179
print ('Formatting .dart files...' );
157
180
// `flutter format` doesn't require the project to actually be a Flutter
158
181
// 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);
162
184
if (exitCode != 0 ) {
163
185
printError ('Failed to format Dart files: exit code $exitCode .' );
164
186
throw ToolExit (_exitFlutterFormatFailed);
165
187
}
166
188
}
167
189
}
168
190
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 {
170
197
// Returns a pattern to check for [directories] as a subset of a file path.
171
198
RegExp pathFragmentForDirectories (List <String > directories) {
172
199
String s = path.separator;
@@ -177,8 +204,10 @@ class FormatCommand extends PluginCommand {
177
204
return RegExp ('(?:^|$s )${path .joinAll (directories )}$s ' );
178
205
}
179
206
207
+ final String fromPath = relativeTo.path;
208
+
180
209
return files
181
- .map ((File file) => file.path)
210
+ .map ((File file) => path. relative ( file.path, from : fromPath) )
182
211
.where ((String path) =>
183
212
// Ignore files in build/ directories (e.g., headers of frameworks)
184
213
// to avoid useless extra work in local repositories.
@@ -212,4 +241,74 @@ class FormatCommand extends PluginCommand {
212
241
213
242
return javaFormatterPath;
214
243
}
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
+ }
215
314
}
0 commit comments