blob: 1e26032564291649bebdb3b6a8096650b40b748a [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Alexander Aprelev391e91c2018-08-30 07:30:25 -07002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
Dan Field24f39d42020-01-02 11:47:28 -08007import 'dart:core' hide print;
8import 'dart:io' hide exit;
Alexander Aprelev391e91c2018-08-30 07:30:25 -07009
10import 'package:path/path.dart' as path;
11
Dan Field24f39d42020-01-02 11:47:28 -080012import 'utils.dart';
Alexander Aprelev391e91c2018-08-30 07:30:25 -070013
Dan Field24f39d42020-01-02 11:47:28 -080014// TODO(ianh): These two functions should be refactored into something that avoids all this code duplication.
Alexander Aprelev391e91c2018-08-30 07:30:25 -070015
Dan Field20e0f132019-03-06 13:13:45 -080016Stream<String> runAndGetStdout(String executable, List<String> arguments, {
17 String workingDirectory,
18 Map<String, String> environment,
19 bool expectNonZeroExit = false,
20 int expectedExitCode,
21 String failureMessage,
Dan Field24f39d42020-01-02 11:47:28 -080022 bool skip = false,
Dan Field20e0f132019-03-06 13:13:45 -080023}) async* {
24 final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
25 final String relativeWorkingDir = path.relative(workingDirectory);
Dan Field24f39d42020-01-02 11:47:28 -080026 if (skip) {
27 printProgress('SKIPPING', relativeWorkingDir, commandDescription);
28 return;
29 }
Dan Field20e0f132019-03-06 13:13:45 -080030 printProgress('RUNNING', relativeWorkingDir, commandDescription);
31
Ian Hicksone0a31de2019-08-20 14:53:39 -070032 final Stopwatch time = Stopwatch()..start();
Dan Field20e0f132019-03-06 13:13:45 -080033 final Process process = await Process.start(executable, arguments,
34 workingDirectory: workingDirectory,
35 environment: environment,
36 );
37
Dan Fieldb9f013c2019-03-10 11:26:17 -070038 stderr.addStream(process.stderr);
Dan Field20e0f132019-03-06 13:13:45 -080039 final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter());
Alexandre Ardhuin1c793472020-01-08 07:34:36 +010040 yield* lines;
Dan Fielddcc965a2019-03-13 12:58:10 -070041
Ian Hicksone0a31de2019-08-20 14:53:39 -070042 final int exitCode = await process.exitCode;
Francisco Magdaleno04ea3182020-01-02 09:25:59 -080043 if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
Dan Field24f39d42020-01-02 11:47:28 -080044 exitWithError(<String>[
45 if (failureMessage != null)
46 failureMessage
47 else
48 '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset',
49 '${bold}Command: $green$commandDescription$reset',
50 '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
51 ]);
Francisco Magdaleno04ea3182020-01-02 09:25:59 -080052 }
Dan Field24f39d42020-01-02 11:47:28 -080053 print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
Dan Field20e0f132019-03-06 13:13:45 -080054}
55
Yegorb3404692020-02-13 18:34:08 -080056/// Runs the `executable` and waits until the process exits.
57///
58/// If the process exits with a non-zero exit code, exits this process with
59/// exit code 1, unless `expectNonZeroExit` is set to true.
60///
61/// `outputListener` is called for every line of standard output from the
62/// process, and is given the [Process] object. This can be used to interrupt
63/// an indefinitely running process, for example, by waiting until the process
64/// emits certain output.
Alexandre Ardhuind340e2f2018-10-04 18:44:23 +020065Future<void> runCommand(String executable, List<String> arguments, {
Alexander Aprelev391e91c2018-08-30 07:30:25 -070066 String workingDirectory,
67 Map<String, String> environment,
68 bool expectNonZeroExit = false,
69 int expectedExitCode,
70 String failureMessage,
James Linc02b8052019-08-09 15:10:45 -070071 OutputMode outputMode = OutputMode.print,
72 CapturedOutput output,
Alexander Aprelev391e91c2018-08-30 07:30:25 -070073 bool skip = false,
Jonah Williams2b203452019-07-10 08:48:01 -070074 bool Function(String) removeLine,
Yegorb3404692020-02-13 18:34:08 -080075 void Function(String, Process) outputListener,
Alexander Aprelev391e91c2018-08-30 07:30:25 -070076}) async {
Dan Field24f39d42020-01-02 11:47:28 -080077 assert(
78 (outputMode == OutputMode.capture) == (output != null),
79 'The output parameter must be non-null with and only with OutputMode.capture',
80 );
James Linc02b8052019-08-09 15:10:45 -070081
Alexander Aprelev391e91c2018-08-30 07:30:25 -070082 final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
83 final String relativeWorkingDir = path.relative(workingDirectory);
84 if (skip) {
85 printProgress('SKIPPING', relativeWorkingDir, commandDescription);
Alexandre Ardhuin8b0de382018-10-17 11:01:37 +020086 return;
Alexander Aprelev391e91c2018-08-30 07:30:25 -070087 }
88 printProgress('RUNNING', relativeWorkingDir, commandDescription);
89
Ian Hicksone0a31de2019-08-20 14:53:39 -070090 final Stopwatch time = Stopwatch()..start();
Alexander Aprelev391e91c2018-08-30 07:30:25 -070091 final Process process = await Process.start(executable, arguments,
92 workingDirectory: workingDirectory,
93 environment: environment,
94 );
95
96 Future<List<List<int>>> savedStdout, savedStderr;
Jonah Williams2b203452019-07-10 08:48:01 -070097 final Stream<List<int>> stdoutSource = process.stdout
98 .transform<String>(const Utf8Decoder())
99 .transform(const LineSplitter())
100 .where((String line) => removeLine == null || !removeLine(line))
Yegorb3404692020-02-13 18:34:08 -0800101 .map((String line) {
102 final String formattedLine = '$line\n';
103 if (outputListener != null) {
104 outputListener(formattedLine, process);
105 }
106 return formattedLine;
107 })
Jonah Williams2b203452019-07-10 08:48:01 -0700108 .transform(const Utf8Encoder());
James Line3ffa762019-08-09 17:01:07 -0700109 switch (outputMode) {
110 case OutputMode.print:
111 await Future.wait<void>(<Future<void>>[
112 stdout.addStream(stdoutSource),
113 stderr.addStream(process.stderr),
114 ]);
115 break;
116 case OutputMode.capture:
117 case OutputMode.discard:
118 savedStdout = stdoutSource.toList();
119 savedStderr = process.stderr.toList();
120 break;
Alexander Aprelev391e91c2018-08-30 07:30:25 -0700121 }
122
Ian Hicksone0a31de2019-08-20 14:53:39 -0700123 final int exitCode = await process.exitCode;
James Linc02b8052019-08-09 15:10:45 -0700124 if (output != null) {
James Lin9823b3d2019-08-15 11:12:08 -0700125 output.stdout = _flattenToString(await savedStdout);
126 output.stderr = _flattenToString(await savedStderr);
James Linc02b8052019-08-09 15:10:45 -0700127 }
128
Alexander Aprelev391e91c2018-08-30 07:30:25 -0700129 if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
James Linc02b8052019-08-09 15:10:45 -0700130 // Print the output when we get unexpected results (unless output was
131 // printed already).
James Line3ffa762019-08-09 17:01:07 -0700132 switch (outputMode) {
133 case OutputMode.print:
134 break;
135 case OutputMode.capture:
136 case OutputMode.discard:
James Lin9823b3d2019-08-15 11:12:08 -0700137 stdout.writeln(_flattenToString(await savedStdout));
138 stderr.writeln(_flattenToString(await savedStderr));
James Line3ffa762019-08-09 17:01:07 -0700139 break;
Alexander Aprelev391e91c2018-08-30 07:30:25 -0700140 }
Dan Field24f39d42020-01-02 11:47:28 -0800141 exitWithError(<String>[
142 if (failureMessage != null)
143 failureMessage
144 else
145 '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset',
146 '${bold}Command: $green$commandDescription$reset',
147 '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
148 ]);
Alexander Aprelev391e91c2018-08-30 07:30:25 -0700149 }
Dan Field24f39d42020-01-02 11:47:28 -0800150 print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
Alexander Aprelev391e91c2018-08-30 07:30:25 -0700151}
James Linc02b8052019-08-09 15:10:45 -0700152
James Linc02b8052019-08-09 15:10:45 -0700153/// Flattens a nested list of UTF-8 code units into a single string.
James Lin9823b3d2019-08-15 11:12:08 -0700154String _flattenToString(List<List<int>> chunks) =>
155 utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
James Linc02b8052019-08-09 15:10:45 -0700156
157/// Specifies what to do with command output from [runCommand].
158enum OutputMode { print, capture, discard }
159
160/// Stores command output from [runCommand] when used with [OutputMode.capture].
161class CapturedOutput {
162 String stdout;
163 String stderr;
164}