@@ -7,17 +7,31 @@ import 'dart:io' as io;
77
88import 'package:file/file.dart' ;
99import 'package:http/http.dart' as http;
10+ import 'package:meta/meta.dart' ;
1011import 'package:platform/platform.dart' ;
11- import 'package:quiver/iterables.dart' ;
1212
1313import 'common/core.dart' ;
1414import 'common/plugin_command.dart' ;
1515import '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+
1730const int _exitClangFormatFailed = 3 ;
1831const int _exitFlutterFormatFailed = 4 ;
1932const int _exitJavaFormatFailed = 5 ;
2033const int _exitGitFailed = 6 ;
34+ const int _exitDependencyMissing = 7 ;
2135
2236final 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}
0 commit comments