run_command.dart 6.75 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as path;

final bool hasColor = stdout.supportsAnsiEscapes;

13 14 15 16 17 18
final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles
final String red = hasColor ? '\x1B[31m' : ''; // used for errors
final String green = hasColor ? '\x1B[32m' : ''; // used for section titles, commands
final String yellow = hasColor ? '\x1B[33m' : ''; // unused
final String cyan = hasColor ? '\x1B[36m' : ''; // used for paths
final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks
19 20 21
final String reset = hasColor ? '\x1B[0m' : '';
final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';

22 23 24 25 26 27 28 29
String get clock {
  final DateTime now = DateTime.now();
  return '$reverse▌'
         '${now.hour.toString().padLeft(2, "0")}:'
         '${now.minute.toString().padLeft(2, "0")}:'
         '${now.second.toString().padLeft(2, "0")}'
         '▐$reset';
}
30

31 32 33 34 35 36 37 38 39
String prettyPrintDuration(Duration duration) {
  String result = '';
  final int minutes = duration.inMinutes;
  if (minutes > 0)
    result += '${minutes}min ';
  final int seconds = duration.inSeconds - minutes * 60;
  final int milliseconds = duration.inMilliseconds - (seconds * 1000 + minutes * 60 * 1000);
  result += '$seconds.${milliseconds.toString().padLeft(3, "0")}s';
  return result;
40 41 42
}

void printProgress(String action, String workingDir, String command) {
43
  print('$clock $action: cd $cyan$workingDir$reset; $green$command$reset');
44 45
}

Dan Field's avatar
Dan Field committed
46 47 48 49 50 51
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
52
  Function beforeExit,
Dan Field's avatar
Dan Field committed
53 54 55 56 57 58
}) async* {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);

  printProgress('RUNNING', relativeWorkingDir, commandDescription);

59
  final Stopwatch time = Stopwatch()..start();
Dan Field's avatar
Dan Field committed
60 61 62 63 64
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

65
  stderr.addStream(process.stderr);
Dan Field's avatar
Dan Field committed
66 67 68 69
  final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter());
  await for (String line in lines) {
    yield line;
  }
70

71 72
  final int exitCode = await process.exitCode;
  print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
Dan Field's avatar
Dan Field committed
73 74 75 76 77 78
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
    if (failureMessage != null) {
      print(failureMessage);
    }
    print(
        '$redLine\n'
79 80 81 82
        '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n'
        '${bold}Command: $green$commandDescription$reset\n'
        '${bold}Relative working directory: $cyan$relativeWorkingDir$reset\n'
        '$redLine'
Dan Field's avatar
Dan Field committed
83
    );
84
    beforeExit?.call();
Dan Field's avatar
Dan Field committed
85 86 87 88
    exit(1);
  }
}

89
Future<void> runCommand(String executable, List<String> arguments, {
90 91 92 93 94
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
95 96
  OutputMode outputMode = OutputMode.print,
  CapturedOutput output,
97
  bool skip = false,
98
  bool Function(String) removeLine,
99
}) async {
James Lin's avatar
James Lin committed
100 101 102
  assert((outputMode == OutputMode.capture) == (output != null),
      'The output parameter must be non-null with and only with '
      'OutputMode.capture');
103

104 105 106 107
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);
  if (skip) {
    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
108
    return;
109 110 111
  }
  printProgress('RUNNING', relativeWorkingDir, commandDescription);

112
  final Stopwatch time = Stopwatch()..start();
113 114 115 116 117 118
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

  Future<List<List<int>>> savedStdout, savedStderr;
119 120 121 122
  final Stream<List<int>> stdoutSource = process.stdout
    .transform<String>(const Utf8Decoder())
    .transform(const LineSplitter())
    .where((String line) => removeLine == null || !removeLine(line))
123
    .map((String line) => '$line\n')
124
    .transform(const Utf8Encoder());
125 126 127 128 129 130 131 132 133 134 135 136
  switch (outputMode) {
    case OutputMode.print:
      await Future.wait<void>(<Future<void>>[
        stdout.addStream(stdoutSource),
        stderr.addStream(process.stderr),
      ]);
      break;
    case OutputMode.capture:
    case OutputMode.discard:
      savedStdout = stdoutSource.toList();
      savedStderr = process.stderr.toList();
      break;
137 138
  }

139 140
  final int exitCode = await process.exitCode;
  print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
141 142

  if (output != null) {
143 144
    output.stdout = _flattenToString(await savedStdout);
    output.stderr = _flattenToString(await savedStderr);
145 146
  }

147 148 149 150
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
    if (failureMessage != null) {
      print(failureMessage);
    }
151 152 153

    // Print the output when we get unexpected results (unless output was
    // printed already).
154 155 156 157 158
    switch (outputMode) {
      case OutputMode.print:
        break;
      case OutputMode.capture:
      case OutputMode.discard:
159 160
        stdout.writeln(_flattenToString(await savedStdout));
        stderr.writeln(_flattenToString(await savedStderr));
161
        break;
162 163 164
    }
    print(
        '$redLine\n'
165 166 167 168
        '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n'
        '${bold}Command: $green$commandDescription$reset\n'
        '${bold}Relative working directory: $cyan$relativeWorkingDir$reset\n'
        '$redLine'
169 170 171 172
    );
    exit(1);
  }
}
173 174

/// Flattens a nested list of UTF-8 code units into a single string.
175 176
String _flattenToString(List<List<int>> chunks) =>
  utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
177 178 179 180 181 182 183 184 185

/// Specifies what to do with command output from [runCommand].
enum OutputMode { print, capture, discard }

/// Stores command output from [runCommand] when used with [OutputMode.capture].
class CapturedOutput {
  String stdout;
  String stderr;
}