utils.dart 20.5 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
// 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';
8
import 'dart:math' as math;
9

10
import 'package:args/args.dart';
11 12
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
13
import 'package:process/process.dart';
14 15
import 'package:stack_trace/stack_trace.dart';

16 17
import 'framework.dart';

18 19 20
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;

21 22 23 24 25 26 27
/// The local engine to use for [flutter] and [evalFlutter], if any.
String get localEngine => const String.fromEnvironment('localEngine');

/// The local engine source path to use if a local engine is used for [flutter]
/// and [evalFlutter].
String get localEngineSrcPath => const String.fromEnvironment('localEngineSrcPath');

28
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
29
ProcessManager _processManager = const LocalProcessManager();
30 31 32 33

class ProcessInfo {
  ProcessInfo(this.command, this.process);

34
  final DateTime startTime = DateTime.now();
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
  final String command;
  final Process process;

  @override
  String toString() {
    return '''
  command : $command
  started : $startTime
  pid     : ${process.pid}
'''
        .trim();
  }
}

/// Result of a health check for a specific parameter.
class HealthCheckResult {
  HealthCheckResult.success([this.details]) : succeeded = true;
  HealthCheckResult.failure(this.details) : succeeded = false;
  HealthCheckResult.error(dynamic error, dynamic stackTrace)
      : succeeded = false,
        details = 'ERROR: $error${'\n$stackTrace' ?? ''}';

  final bool succeeded;
  final String details;

  @override
  String toString() {
62
    final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
63 64 65
    if (details != null && details.trim().isNotEmpty) {
      buf.writeln();
      // Indent details by 4 spaces
66
      for (final String line in details.trim().split('\n')) {
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
        buf.writeln('    $line');
      }
    }
    return '$buf';
  }
}

class BuildFailedError extends Error {
  BuildFailedError(this.message);

  final String message;

  @override
  String toString() => message;
}

void fail(String message) {
84
  throw BuildFailedError(message);
85 86
}

87 88 89 90 91 92 93 94 95 96 97 98
// Remove the given file or directory.
void rm(FileSystemEntity entity, { bool recursive = false}) {
  if (entity.existsSync()) {
    // This should not be necessary, but it turns out that
    // on Windows it's common for deletions to fail due to
    // bogus (we think) "access denied" errors.
    try {
      entity.deleteSync(recursive: recursive);
    } on FileSystemException catch (error) {
      print('Failed to delete ${entity.path}: $error');
    }
  }
99 100 101 102
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
103
  rm(entity, recursive: true);
104 105 106 107
}

List<FileSystemEntity> ls(Directory directory) => directory.listSync();

108
Directory dir(String path) => Directory(path);
109

110
File file(String path) => File(path);
111 112

void copy(File sourceFile, Directory targetDirectory, {String name}) {
113
  final File target = file(
114 115 116 117
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

118 119 120 121
void recursiveCopy(Directory source, Directory target) {
  if (!target.existsSync())
    target.createSync();

122
  for (final FileSystemEntity entity in source.listSync(followLinks: false)) {
123
    final String name = path.basename(entity.path);
124
    if (entity is Directory && !entity.path.contains('.dart_tool'))
125
      recursiveCopy(entity, Directory(path.join(target.path, name)));
126
    else if (entity is File) {
127
      final File dest = File(path.join(target.path, name));
128
      dest.writeAsBytesSync(entity.readAsBytesSync());
129 130 131 132 133
      // Preserve executable bit
      final String modes = entity.statSync().modeString();
      if (modes != null && modes.contains('x')) {
        makeExecutable(dest);
      }
134 135 136 137
    }
  }
}

138 139 140 141 142 143
FileSystemEntity move(FileSystemEntity whatToMove,
    {Directory to, String name}) {
  return whatToMove
      .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
/// Equivalent of `chmod a+x file`
void makeExecutable(File file) {
  // Windows files do not have an executable bit
  if (Platform.isWindows) {
    return;
  }
  final ProcessResult result = _processManager.runSync(<String>[
    'chmod',
    'a+x',
    file.path,
  ]);

  if (result.exitCode != 0) {
    throw FileSystemException(
      'Error making ${file.path} executable.\n'
      '${result.stderr}',
      file.path,
    );
  }
}

165 166 167 168 169 170 171 172 173 174 175 176 177
/// Equivalent of `mkdir directory`.
void mkdir(Directory directory) {
  directory.createSync();
}

/// Equivalent of `mkdir -p directory`.
void mkdirs(Directory directory) {
  directory.createSync(recursive: true);
}

bool exists(FileSystemEntity entity) => entity.existsSync();

void section(String title) {
178 179 180 181 182 183
  title = '╡ ••• $title ••• ╞';
  final String line = '═' * math.max((80 - title.length) ~/ 2, 2);
  String output = '$line$title$line';
  if (output.length == 79)
    output += '═';
  print('\n\n$output\n');
184 185 186 187
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
188
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
189
  String version = (result.stderr as String).trim();
190 191 192 193 194

  // Convert:
  //   Dart VM version: 1.17.0-dev.2.0 (Tue May  3 12:14:52 2016) on "macos_x64"
  // to:
  //   1.17.0-dev.2.0
195
  if (version.contains('('))
196
    version = version.substring(0, version.indexOf('(')).trim();
197
  if (version.contains(':'))
198 199 200 201 202 203 204
    version = version.substring(version.indexOf(':') + 1).trim();

  return version.replaceAll('"', "'");
}

Future<String> getCurrentFlutterRepoCommit() {
  if (!dir('${flutterDirectory.path}/.git').existsSync()) {
205
    return Future<String>.value(null);
206 207
  }

208
  return inDirectory<String>(flutterDirectory, () {
209 210 211 212 213 214
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
  // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
215
  return inDirectory<DateTime>(flutterDirectory, () async {
216
    final String unixTimestamp = await eval('git', <String>[
217 218 219 220 221
      'show',
      '-s',
      '--format=%at',
      commit,
    ]);
222
    final int secondsSinceEpoch = int.parse(unixTimestamp);
223
    return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
224 225 226
  });
}

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
/// Starts a subprocess.
///
/// The first argument is the full path to the executable to run.
///
/// The second argument is the list of arguments to provide on the command line.
/// This argument can be null, indicating no arguments (same as the empty list).
///
/// The `environment` argument can be provided to configure environment variables
/// that will be made available to the subprocess. The `BOT` environment variable
/// is always set and overrides any value provided in the `environment` argument.
/// The `isBot` argument controls the value of the `BOT` variable. It will either
/// be "true", if `isBot` is true (the default), or "false" if it is false.
///
/// The `BOT` variable is in particular used by the `flutter` tool to determine
/// how verbose to be and whether to enable analytics by default.
///
/// The working directory can be provided using the `workingDirectory` argument.
/// By default it will default to the current working directory (see [cwd]).
///
/// Information regarding the execution of the subprocess is printed to the
/// console.
///
/// The actual process executes asynchronously. A handle to the subprocess is
/// returned in the form of a [Future] that completes to a [Process] object.
251 252 253 254
Future<Process> startProcess(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
255
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
256 257
  String workingDirectory,
}) async {
258
  assert(isBot != null);
259
  final String command = '$executable ${arguments?.join(" ") ?? ""}';
260
  final String finalWorkingDirectory = workingDirectory ?? cwd;
261 262
  print('\nExecuting: $command in $finalWorkingDirectory'
      + (environment != null ? ' with environment $environment' : ''));
263
  environment ??= <String, String>{};
264
  environment['BOT'] = isBot ? 'true' : 'false';
265
  final Process process = await _processManager.start(
266
    <String>[executable, ...arguments],
267
    environment: environment,
268
    workingDirectory: finalWorkingDirectory,
269
  );
270
  final ProcessInfo processInfo = ProcessInfo(command, process);
271 272
  _runningProcesses.add(processInfo);

273
  process.exitCode.then<void>((int exitCode) {
274
    print('"$executable" exit code: $exitCode');
275
    _runningProcesses.remove(processInfo);
276 277
  });

278
  return process;
279 280
}

281
Future<void> forceQuitRunningProcesses() async {
282 283 284 285
  if (_runningProcesses.isEmpty)
    return;

  // Give normally quitting processes a chance to report their exit code.
286
  await Future<void>.delayed(const Duration(seconds: 1));
287 288

  // Whatever's left, kill it.
289
  for (final ProcessInfo p in _runningProcesses) {
290
    print('Force-quitting process:\n$p');
291 292 293 294 295 296 297 298
    if (!p.process.kill()) {
      print('Failed to force quit process');
    }
  }
  _runningProcesses.clear();
}

/// Executes a command and returns its exit code.
299 300 301 302
Future<int> exec(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
303
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
304
  String workingDirectory,
305
}) async {
306
  final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory);
307

308 309
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
310
  process.stdout
311 312
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
313 314 315
      .listen((String line) {
        print('stdout: $line');
      }, onDone: () { stdoutDone.complete(); });
316
  process.stderr
317 318
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
319 320 321
      .listen((String line) {
        print('stderr: $line');
      }, onDone: () { stderrDone.complete(); });
322

323
  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
324
  final int exitCode = await process.exitCode;
325 326

  if (exitCode != 0 && !canFail)
327
    fail('Executable "$executable" failed with exit code $exitCode.');
328 329 330 331 332 333

  return exitCode;
}

/// Executes a command and returns its standard output as a String.
///
334
/// For logging purposes, the command's output is also printed out by default.
335 336 337 338
Future<String> eval(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
339
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
340
  String workingDirectory,
341
  StringBuffer stderr, // if not null, the stderr will be written here
342 343
  bool printStdout = true,
  bool printStderr = true,
344
}) async {
345
  final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory);
346

347
  final StringBuffer output = StringBuffer();
348 349
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
350
  process.stdout
351 352
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
353
      .listen((String line) {
354 355 356
        if (printStdout) {
          print('stdout: $line');
        }
357 358 359
        output.writeln(line);
      }, onDone: () { stdoutDone.complete(); });
  process.stderr
360 361
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
362
      .listen((String line) {
363 364 365
        if (printStderr) {
          print('stderr: $line');
        }
366
        stderr?.writeln(line);
367 368
      }, onDone: () { stderrDone.complete(); });

369
  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
370
  final int exitCode = await process.exitCode;
371 372

  if (exitCode != 0 && !canFail)
373
    fail('Executable "$executable" failed with exit code $exitCode.');
374

375
  return output.toString().trimRight();
376 377
}

378 379
List<String> flutterCommandArgs(String command, List<String> options) {
  return <String>[
380 381 382 383 384
    command,
    if (localEngine != null) ...<String>['--local-engine', localEngine],
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
    ...options,
  ];
385 386 387 388 389 390 391 392
}

Future<int> flutter(String command, {
  List<String> options = const <String>[],
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
  Map<String, String> environment,
}) {
  final List<String> args = flutterCommandArgs(command, options);
393
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
394
      canFail: canFail, environment: environment);
395 396
}

397
/// Runs a `flutter` command and returns the standard output as a string.
398
Future<String> evalFlutter(String command, {
399
  List<String> options = const <String>[],
400
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
401
  Map<String, String> environment,
402
  StringBuffer stderr, // if not null, the stderr will be written here.
403
}) {
404
  final List<String> args = flutterCommandArgs(command, options);
405
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
406
      canFail: canFail, environment: environment, stderr: stderr);
407 408
}

409 410 411 412 413
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

Future<int> dart(List<String> args) => exec(dartBin, args);

414 415 416 417 418 419 420 421 422 423 424 425 426 427
/// Returns a future that completes with a path suitable for JAVA_HOME
/// or with null, if Java cannot be found.
Future<String> findJavaHome() async {
  final Iterable<String> hits = grep(
    'Java binary at: ',
    from: await evalFlutter('doctor', options: <String>['-v']),
  );
  if (hits.isEmpty)
    return null;
  final String javaBinary = hits.first.split(': ').last;
  // javaBinary == /some/path/to/java/home/bin/java
  return path.dirname(path.dirname(javaBinary));
}

428
Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async {
429
  final String previousCwd = cwd;
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
  try {
    cd(directory);
    return await action();
  } finally {
    cd(previousCwd);
  }
}

void cd(dynamic directory) {
  Directory d;
  if (directory is String) {
    cwd = directory;
    d = dir(directory);
  } else if (directory is Directory) {
    cwd = directory.path;
    d = directory;
  } else {
    throw 'Unsupported type ${directory.runtimeType} of $directory';
  }

  if (!d.existsSync())
    throw 'Cannot cd into directory that does not exist: $directory';
}

454
Directory get flutterDirectory => Directory.current.parent.parent;
455 456

String requireEnvVar(String name) {
457
  final String value = Platform.environment[name];
458

459 460
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
461 462 463 464

  return value;
}

465
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
466 467
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
468
  final T result = map[propertyName] as T;
469
  return result;
470 471 472
}

String jsonEncode(dynamic data) {
473
  return const JsonEncoder.withIndent('  ').convert(data) + '\n';
474 475
}

476
Future<void> getFlutter(String revision) async {
477 478 479
  section('Get Flutter!');

  if (exists(flutterDirectory)) {
480
    flutterDirectory.deleteSync(recursive: true);
481 482
  }

483
  await inDirectory<void>(flutterDirectory.parent, () async {
484 485 486
    await exec('git', <String>['clone', 'https://github.com/flutter/flutter.git']);
  });

487
  await inDirectory<void>(flutterDirectory, () async {
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
    await exec('git', <String>['checkout', revision]);
  });

  await flutter('config', options: <String>['--no-analytics']);

  section('flutter doctor');
  await flutter('doctor');

  section('flutter update-packages');
  await flutter('update-packages');
}

void checkNotNull(Object o1,
    [Object o2 = 1,
    Object o3 = 1,
    Object o4 = 1,
    Object o5 = 1,
    Object o6 = 1,
    Object o7 = 1,
    Object o8 = 1,
    Object o9 = 1,
    Object o10 = 1]) {
  if (o1 == null)
    throw 'o1 is null';
  if (o2 == null)
    throw 'o2 is null';
  if (o3 == null)
    throw 'o3 is null';
  if (o4 == null)
    throw 'o4 is null';
  if (o5 == null)
    throw 'o5 is null';
  if (o6 == null)
    throw 'o6 is null';
  if (o7 == null)
    throw 'o7 is null';
  if (o8 == null)
    throw 'o8 is null';
  if (o9 == null)
    throw 'o9 is null';
  if (o10 == null)
    throw 'o10 is null';
}

/// Splits [from] into lines and selects those that contain [pattern].
Iterable<String> grep(Pattern pattern, {@required String from}) {
  return from.split('\n').where((String line) {
    return line.contains(pattern);
  });
}

/// Captures asynchronous stack traces thrown by [callback].
///
/// This is a convenience wrapper around [Chain] optimized for use with
/// `async`/`await`.
///
/// Example:
///
///     try {
///       await captureAsyncStacks(() { /* async things */ });
///     } catch (error, chain) {
///
///     }
551 552
Future<void> runAndCaptureAsyncStacks(Future<void> callback()) {
  final Completer<void> completer = Completer<void>();
553 554 555
  Chain.capture(() async {
    await callback();
    completer.complete();
556
  }, onError: completer.completeError);
557 558
  return completer.future;
}
559

560
bool canRun(String path) => _processManager.canRun(path);
561 562

String extractCloudAuthTokenArg(List<String> rawArgs) {
563
  final ArgParser argParser = ArgParser()..addOption('cloud-auth-token');
564 565 566
  ArgResults args;
  try {
    args = argParser.parse(rawArgs);
567
  } on FormatException catch (error) {
568 569 570 571 572 573
    stderr.writeln('${error.message}\n');
    stderr.writeln('Usage:\n');
    stderr.writeln(argParser.usage);
    return null;
  }

574
  final String token = args['cloud-auth-token'] as String;
575 576 577 578 579 580
  if (token == null) {
    stderr.writeln('Required option --cloud-auth-token not found');
    return null;
  }
  return token;
}
581

582 583 584 585 586
final RegExp _obsRegExp =
  RegExp('An Observatory debugger .* is available at: ');
final RegExp _obsPortRegExp = RegExp('(\\S+:(\\d+)/\\S*)\$');
final RegExp _obsUriRegExp = RegExp('((http|\/\/)[a-zA-Z0-9:/=_\\-\.\\[\\]]+)');

587 588 589
/// Tries to extract a port from the string.
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
590
/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `.
591
int parseServicePort(String line, {
592
  Pattern prefix,
593
}) {
594
  prefix ??= _obsRegExp;
595 596
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
597 598
    return null;
  }
599
  final Match prefixMatch = matchesIter.first;
600 601 602 603 604
  final List<Match> matches =
    _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : int.parse(matches[0].group(2));
}

605
/// Tries to extract a URL from the string.
606 607 608 609 610 611 612
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `.
Uri parseServiceUri(String line, {
  Pattern prefix,
}) {
  prefix ??= _obsRegExp;
613 614
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
615 616
    return null;
  }
617
  final Match prefixMatch = matchesIter.first;
618 619 620
  final List<Match> matches =
    _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : Uri.parse(matches[0].group(0));
621
}
622

623 624 625
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
626
    throw FileSystemException('Expected file to exist.', file);
627 628
  }
}
629

xster's avatar
xster committed
630 631 632
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
633
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
634 635 636
  }
}

637 638
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
639
  for (final T value in values) {
640 641 642
    if (!collection.contains(value)) {
      throw TaskResult.failure('Expected to find `$value` in `${collection.toString()}`.');
    }
643 644 645
  }
}

646 647
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
648
  for (final T value in values) {
649 650 651 652
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
653 654
}

655 656 657 658
/// Checks that the contents of a [File] at `filePath` contains the specified
/// [Pattern]s, otherwise throws a [TaskResult].
void checkFileContains(List<Pattern> patterns, String filePath) {
  final String fileContent = File(filePath).readAsStringSync();
659
  for (final Pattern pattern in patterns) {
660 661 662 663 664 665 666
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
667
}