Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 09e3464

Browse files
committed
Convert to using process_runner package
1 parent 6f26771 commit 09e3464

File tree

2 files changed

+41
-276
lines changed

2 files changed

+41
-276
lines changed

ci/bin/lint.dart

Lines changed: 39 additions & 275 deletions
Original file line numberDiff line numberDiff line change
@@ -7,267 +7,11 @@
77
88
import 'dart:async' show Completer;
99
import 'dart:convert' show jsonDecode, utf8, LineSplitter;
10-
import 'dart:io'
11-
show
12-
File,
13-
Process,
14-
ProcessResult,
15-
ProcessException,
16-
exit,
17-
Directory,
18-
FileSystemEntity,
19-
Platform,
20-
stderr,
21-
stdout;
10+
import 'dart:io' show File, exit, Directory, FileSystemEntity, Platform, stderr;
2211

2312
import 'package:args/args.dart';
2413
import 'package:path/path.dart' as path;
25-
26-
Platform defaultPlatform = Platform();
27-
28-
/// Exception class for when a process fails to run, so we can catch
29-
/// it and provide something more readable than a stack trace.
30-
class ProcessRunnerException implements Exception {
31-
ProcessRunnerException(this.message, {this.result});
32-
33-
final String message;
34-
final ProcessResult result;
35-
36-
int get exitCode => result?.exitCode ?? -1;
37-
38-
@override
39-
String toString() {
40-
String output = runtimeType.toString();
41-
output += ': $message';
42-
final String stderr = (result?.stderr ?? '') as String;
43-
if (stderr.isNotEmpty) {
44-
output += ':\n$stderr';
45-
}
46-
return output;
47-
}
48-
}
49-
50-
class ProcessRunnerResult {
51-
const ProcessRunnerResult(this.exitCode, this.stdout, this.stderr, this.output);
52-
final int exitCode;
53-
final List<int> stdout;
54-
final List<int> stderr;
55-
final List<int> output;
56-
}
57-
58-
/// A helper class for classes that want to run a process, optionally have the
59-
/// stderr and stdout reported as the process runs, and capture the stdout
60-
/// properly without dropping any.
61-
class ProcessRunner {
62-
ProcessRunner({
63-
this.defaultWorkingDirectory,
64-
});
65-
66-
/// Sets the default directory used when `workingDirectory` is not specified
67-
/// to [runProcess].
68-
final Directory defaultWorkingDirectory;
69-
70-
/// The environment to run processes with.
71-
Map<String, String> environment = Map<String, String>.from(Platform.environment);
72-
73-
/// Run the command and arguments in `commandLine` as a sub-process from
74-
/// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses
75-
/// [Directory.current] if [defaultWorkingDirectory] is not set.
76-
///
77-
/// Set `failOk` if [runProcess] should not throw an exception when the
78-
/// command completes with a a non-zero exit code.
79-
Future<ProcessRunnerResult> runProcess(
80-
List<String> commandLine, {
81-
Directory workingDirectory,
82-
bool printOutput = false,
83-
bool failOk = false,
84-
Stream<List<int>> stdin,
85-
}) async {
86-
workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
87-
if (printOutput) {
88-
stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
89-
}
90-
final List<int> stdoutOutput = <int>[];
91-
final List<int> stderrOutput = <int>[];
92-
final List<int> combinedOutput = <int>[];
93-
final Completer<void> stdoutComplete = Completer<void>();
94-
final Completer<void> stderrComplete = Completer<void>();
95-
final Completer<void> stdinComplete = Completer<void>();
96-
97-
Process process;
98-
Future<int> allComplete() async {
99-
if (stdin != null) {
100-
await stdinComplete.future;
101-
await process?.stdin?.close();
102-
}
103-
await stderrComplete.future;
104-
await stdoutComplete.future;
105-
return process?.exitCode ?? Future<int>.value(0);
106-
}
107-
108-
try {
109-
process = await Process.start(
110-
commandLine.first,
111-
commandLine.sublist(1),
112-
workingDirectory: workingDirectory.absolute.path,
113-
environment: environment,
114-
runInShell: false,
115-
);
116-
if (stdin != null) {
117-
stdin.listen((List<int> data) {
118-
process?.stdin?.add(data);
119-
}, onDone: () async => stdinComplete.complete());
120-
}
121-
process.stdout.listen(
122-
(List<int> event) {
123-
stdoutOutput.addAll(event);
124-
combinedOutput.addAll(event);
125-
if (printOutput) {
126-
stdout.add(event);
127-
}
128-
},
129-
onDone: () async => stdoutComplete.complete(),
130-
);
131-
process.stderr.listen(
132-
(List<int> event) {
133-
stderrOutput.addAll(event);
134-
combinedOutput.addAll(event);
135-
if (printOutput) {
136-
stderr.add(event);
137-
}
138-
},
139-
onDone: () async => stderrComplete.complete(),
140-
);
141-
} on ProcessException catch (e) {
142-
final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
143-
'failed with:\n${e.toString()}';
144-
throw ProcessRunnerException(message);
145-
} on ArgumentError catch (e) {
146-
final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
147-
'failed with:\n${e.toString()}';
148-
throw ProcessRunnerException(message);
149-
}
150-
151-
final int exitCode = await allComplete();
152-
if (exitCode != 0 && !failOk) {
153-
final String message =
154-
'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
155-
throw ProcessRunnerException(
156-
message,
157-
result: ProcessResult(
158-
0, exitCode, null, 'exited with code $exitCode\n${utf8.decode(combinedOutput)}'),
159-
);
160-
}
161-
return ProcessRunnerResult(exitCode, stdoutOutput, stderrOutput, combinedOutput);
162-
}
163-
}
164-
165-
class WorkerJob {
166-
WorkerJob(
167-
this.name,
168-
this.args, {
169-
this.workingDirectory,
170-
this.printOutput = false,
171-
});
172-
173-
/// The name of the job.
174-
final String name;
175-
176-
/// The arguments for the process, including the command name as args[0].
177-
final List<String> args;
178-
179-
/// The working directory that the command should be executed in.
180-
final Directory workingDirectory;
181-
182-
/// Whether or not this command should print it's stdout when it runs.
183-
final bool printOutput;
184-
185-
@override
186-
String toString() {
187-
return args.join(' ');
188-
}
189-
}
190-
191-
/// A pool of worker processes that will keep [numWorkers] busy until all of the
192-
/// (presumably single-threaded) processes are finished.
193-
class ProcessPool {
194-
ProcessPool({int numWorkers}) : numWorkers = numWorkers ?? Platform.numberOfProcessors;
195-
196-
ProcessRunner processRunner = ProcessRunner();
197-
int numWorkers;
198-
List<WorkerJob> pendingJobs = <WorkerJob>[];
199-
List<WorkerJob> failedJobs = <WorkerJob>[];
200-
Map<WorkerJob, Future<List<int>>> inProgressJobs = <WorkerJob, Future<List<int>>>{};
201-
Map<WorkerJob, ProcessRunnerResult> completedJobs = <WorkerJob, ProcessRunnerResult>{};
202-
Completer<Map<WorkerJob, ProcessRunnerResult>> completer =
203-
Completer<Map<WorkerJob, ProcessRunnerResult>>();
204-
205-
void _printReport() {
206-
final int totalJobs = completedJobs.length + inProgressJobs.length + pendingJobs.length;
207-
final String percent =
208-
totalJobs == 0 ? '100' : ((100 * completedJobs.length) ~/ totalJobs).toString().padLeft(3);
209-
final String completed = completedJobs.length.toString().padLeft(3);
210-
final String total = totalJobs.toString().padRight(3);
211-
final String inProgress = inProgressJobs.length.toString().padLeft(2);
212-
final String pending = pendingJobs.length.toString().padLeft(3);
213-
stdout.write('Jobs: $percent% done, $completed/$total completed, $inProgress in '
214-
'progress, $pending pending. \r');
215-
}
216-
217-
Future<List<int>> _scheduleJob(WorkerJob job) async {
218-
final Completer<List<int>> jobDone = Completer<List<int>>();
219-
final List<int> output = <int>[];
220-
try {
221-
completedJobs[job] = await processRunner.runProcess(
222-
job.args,
223-
workingDirectory: job.workingDirectory,
224-
printOutput: job.printOutput,
225-
);
226-
} catch (e) {
227-
failedJobs.add(job);
228-
if (e is ProcessRunnerException) {
229-
print(e.toString());
230-
print('${utf8.decode(output)}');
231-
} else {
232-
print('\nJob $job failed: $e');
233-
}
234-
} finally {
235-
inProgressJobs.remove(job);
236-
if (pendingJobs.isNotEmpty) {
237-
final WorkerJob newJob = pendingJobs.removeAt(0);
238-
inProgressJobs[newJob] = _scheduleJob(newJob);
239-
} else {
240-
if (inProgressJobs.isEmpty) {
241-
completer.complete(completedJobs);
242-
}
243-
}
244-
jobDone.complete(output);
245-
_printReport();
246-
}
247-
return jobDone.future;
248-
}
249-
250-
Future<Map<WorkerJob, ProcessRunnerResult>> startWorkers(List<WorkerJob> jobs) async {
251-
assert(inProgressJobs.isEmpty);
252-
assert(failedJobs.isEmpty);
253-
assert(completedJobs.isEmpty);
254-
if (jobs.isEmpty) {
255-
return <WorkerJob, ProcessRunnerResult>{};
256-
}
257-
pendingJobs = jobs;
258-
for (int i = 0; i < numWorkers; ++i) {
259-
if (pendingJobs.isEmpty) {
260-
break;
261-
}
262-
final WorkerJob job = pendingJobs.removeAt(0);
263-
inProgressJobs[job] = _scheduleJob(job);
264-
}
265-
return completer.future.then((Map<WorkerJob, ProcessRunnerResult> result) {
266-
stdout.flush();
267-
return result;
268-
});
269-
}
270-
}
14+
import 'package:process_runner/process_runner.dart';
27115

27216
String _linterOutputHeader = '''
27317
┌──────────────────────────┐
@@ -313,20 +57,33 @@ bool containsAny(File file, Iterable<File> queries) {
31357
return queries.where((File query) => path.equals(query.path, file.path)).isNotEmpty;
31458
}
31559

316-
/// Returns a list of all files with current changes or differ from `master`.
60+
/// Returns a list of all non-deleted files which differ from the nearest
61+
/// merge-base with `master`. If it can't find a fork point, uses the default
62+
/// merge-base.
31763
Future<List<File>> getListOfChangedFiles(Directory repoPath) async {
31864
final ProcessRunner processRunner = ProcessRunner(defaultWorkingDirectory: repoPath);
319-
String branch = 'upstream/master';
320-
final ProcessResult fetchResult = Process.runSync('git', <String>['fetch', 'upstream', 'master']);
65+
final ProcessRunnerResult fetchResult = await processRunner.runProcess(
66+
<String>['git', 'fetch', 'upstream', 'master'],
67+
failOk: true,
68+
);
32169
if (fetchResult.exitCode != 0) {
322-
branch = 'origin/master';
323-
Process.runSync('git', <String>['fetch', 'origin', 'master']);
70+
await processRunner.runProcess(<String>['git', 'fetch', 'origin', 'master']);
32471
}
32572
final Set<String> result = <String>{};
326-
final ProcessRunnerResult diffResult = await processRunner.runProcess(
327-
<String>['git', 'diff', '--name-only', '--diff-filter=ACMRT', if (branch.isNotEmpty) branch]);
328-
329-
result.addAll(utf8.decode(diffResult.stdout).split('\n').where(isNonEmptyString));
73+
ProcessRunnerResult mergeBaseResult = await processRunner.runProcess(
74+
<String>['git', 'merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
75+
failOk: true);
76+
if (mergeBaseResult.exitCode != 0) {
77+
if (verbose) {
78+
stderr.writeln("Didn't find a fork point, falling back to default merge base.");
79+
}
80+
mergeBaseResult = await processRunner
81+
.runProcess(<String>['git', 'merge-base', 'FETCH_HEAD', 'HEAD'], failOk: false);
82+
}
83+
final String mergeBase = mergeBaseResult.stdout.trim();
84+
final ProcessRunnerResult masterResult = await processRunner
85+
.runProcess(<String>['git', 'diff', '--name-only', '--diff-filter=ACMRT', mergeBase]);
86+
result.addAll(masterResult.stdout.split('\n').where(isNonEmptyString));
33087
return result.map<File>((String filePath) => File(path.join(repoPath.path, filePath))).toList();
33188
}
33289

@@ -409,6 +166,8 @@ void main(List<String> arguments) async {
409166
_usage(parser);
410167
}
411168

169+
print(_linterOutputHeader);
170+
412171
final String checksArg = options.wasParsed('checks') ? options['checks'] as String : '';
413172
final String checks = checksArg.isNotEmpty ? '--checks=$checksArg' : '--config=';
414173
final bool lintAll =
@@ -439,12 +198,17 @@ void main(List<String> arguments) async {
439198
final List<Command> changedFileBuildCommands =
440199
buildCommands.where((Command x) => containsAny(x.file, changedFiles)).toList();
441200

201+
if (changedFileBuildCommands.isEmpty) {
202+
print('No changed files that have build commands associated with them '
203+
'were found.');
204+
exit(0);
205+
}
206+
442207
if (verbose) {
443208
print('Found ${changedFileBuildCommands.length} files that have build '
444209
'commands associated with them and can be lint checked.');
445210
}
446211

447-
print(_linterOutputHeader);
448212
int exitCode = 0;
449213
final List<WorkerJob> jobs = <WorkerJob>[];
450214
for (Command command in changedFileBuildCommands) {
@@ -453,23 +217,23 @@ void main(List<String> arguments) async {
453217
final List<String> args = <String>[command.file.path, checks, '--'];
454218
args.addAll(tidyArgs?.split(' ') ?? <String>[]);
455219
print('🔶 linting ${command.file}');
456-
jobs.add(WorkerJob(command.file.path, <String>[tidyPath, ...args],
457-
workingDirectory: command.directory));
220+
jobs.add(WorkerJob(<String>[tidyPath, ...args],
221+
workingDirectory: command.directory, name: 'clang-tidy on ${command.file.path}'));
458222
} else {
459223
print('🔷 ignoring ${command.file}');
460224
}
461225
}
462226
final ProcessPool pool = ProcessPool();
463-
final Map<WorkerJob, ProcessRunnerResult> results = await pool.startWorkers(jobs);
464-
print('\n');
465-
for (final WorkerJob job in results.keys) {
466-
if (results[job].stdout.isEmpty) {
227+
228+
await for (final WorkerJob job in pool.startWorkers(jobs)) {
229+
if (job.result.stdout.isEmpty) {
467230
continue;
468231
}
469232
print('❌ Failures for ${job.name}:');
470-
print(utf8.decode(results[job].stdout));
233+
print(job.result.stdout);
471234
exitCode = 1;
472235
}
236+
print('\n');
473237
if (exitCode == 0) {
474238
print('No lint problems found.');
475239
}

ci/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: ci_scripts
33
dependencies:
44
args: ^1.6.0
55
path: ^1.7.0
6+
process_runner: ^2.0.3
67

78
environment:
8-
sdk: '>=2.8.0 <3.0.0'
9+
sdk: '>=2.8.0 <3.0.0'

0 commit comments

Comments
 (0)