utils.dart 21 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 310 311 312 313 314
  await forwardStandardStreams(process);
  final int exitCode = await process.exitCode;

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

  return exitCode;
}
315

316 317 318 319 320
/// Forwards standard out and standard error from [process] to this process'
/// respective outputs.
///
/// Returns a future that completes when both out and error streams a closed.
Future<void> forwardStandardStreams(Process process) {
321 322
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
323
  process.stdout
324 325
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
326 327 328
      .listen((String line) {
        print('stdout: $line');
      }, onDone: () { stdoutDone.complete(); });
329
  process.stderr
330 331
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
332 333 334
      .listen((String line) {
        print('stderr: $line');
      }, onDone: () { stderrDone.complete(); });
335

336
  return Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
337 338 339 340
}

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

354
  final StringBuffer output = StringBuffer();
355 356
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
357
  process.stdout
358 359
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
360
      .listen((String line) {
361 362 363
        if (printStdout) {
          print('stdout: $line');
        }
364 365 366
        output.writeln(line);
      }, onDone: () { stdoutDone.complete(); });
  process.stderr
367 368
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
369
      .listen((String line) {
370 371 372
        if (printStderr) {
          print('stderr: $line');
        }
373
        stderr?.writeln(line);
374 375
      }, onDone: () { stderrDone.complete(); });

376
  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
377
  final int exitCode = await process.exitCode;
378 379

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

382
  return output.toString().trimRight();
383 384
}

385 386
List<String> flutterCommandArgs(String command, List<String> options) {
  return <String>[
387 388 389 390 391
    command,
    if (localEngine != null) ...<String>['--local-engine', localEngine],
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
    ...options,
  ];
392 393 394 395 396 397 398 399
}

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);
400
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
401
      canFail: canFail, environment: environment);
402 403
}

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

416 417 418 419 420
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

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

421 422 423 424 425 426 427 428 429 430 431 432 433 434
/// 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));
}

435
Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async {
436
  final String previousCwd = cwd;
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
  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';
}

461
Directory get flutterDirectory => Directory.current.parent.parent;
462 463

String requireEnvVar(String name) {
464
  final String value = Platform.environment[name];
465

466 467
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
468 469 470 471

  return value;
}

472
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
473 474
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
475
  final T result = map[propertyName] as T;
476
  return result;
477 478 479
}

String jsonEncode(dynamic data) {
480
  return const JsonEncoder.withIndent('  ').convert(data) + '\n';
481 482
}

483
Future<void> getFlutter(String revision) async {
484 485 486
  section('Get Flutter!');

  if (exists(flutterDirectory)) {
487
    flutterDirectory.deleteSync(recursive: true);
488 489
  }

490
  await inDirectory<void>(flutterDirectory.parent, () async {
491 492 493
    await exec('git', <String>['clone', 'https://github.com/flutter/flutter.git']);
  });

494
  await inDirectory<void>(flutterDirectory, () async {
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 551 552 553 554 555 556 557
    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) {
///
///     }
558 559
Future<void> runAndCaptureAsyncStacks(Future<void> callback()) {
  final Completer<void> completer = Completer<void>();
560 561 562
  Chain.capture(() async {
    await callback();
    completer.complete();
563
  }, onError: completer.completeError);
564 565
  return completer.future;
}
566

567
bool canRun(String path) => _processManager.canRun(path);
568 569

String extractCloudAuthTokenArg(List<String> rawArgs) {
570
  final ArgParser argParser = ArgParser()..addOption('cloud-auth-token');
571 572 573
  ArgResults args;
  try {
    args = argParser.parse(rawArgs);
574
  } on FormatException catch (error) {
575 576 577 578 579 580
    stderr.writeln('${error.message}\n');
    stderr.writeln('Usage:\n');
    stderr.writeln(argParser.usage);
    return null;
  }

581
  final String token = args['cloud-auth-token'] as String;
582 583 584 585 586 587
  if (token == null) {
    stderr.writeln('Required option --cloud-auth-token not found');
    return null;
  }
  return token;
}
588

589 590
final RegExp _obsRegExp =
  RegExp('An Observatory debugger .* is available at: ');
591
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
592
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
593

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

612
/// Tries to extract a URL from the string.
613 614 615 616 617 618 619
///
/// 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;
620 621
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
622 623
    return null;
  }
624
  final Match prefixMatch = matchesIter.first;
625 626 627
  final List<Match> matches =
    _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
  return matches.isEmpty ? null : Uri.parse(matches[0].group(0));
628
}
629

630 631 632
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
633
    throw FileSystemException('Expected file to exist.', file);
634 635
  }
}
636

xster's avatar
xster committed
637 638 639
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
640
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
641 642 643
  }
}

644 645 646 647 648 649 650
/// Checks that the directory exists, otherwise throws a [FileSystemException].
void checkDirectoryExists(String directory) {
  if (!exists(Directory(directory))) {
    throw FileSystemException('Expected directory to exist.', directory);
  }
}

651 652
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
653
  for (final T value in values) {
654 655 656
    if (!collection.contains(value)) {
      throw TaskResult.failure('Expected to find `$value` in `${collection.toString()}`.');
    }
657 658 659
  }
}

660 661
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
662
  for (final T value in values) {
663 664 665 666
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
667 668
}

669 670 671 672
/// 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();
673
  for (final Pattern pattern in patterns) {
674 675 676 677 678 679 680
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
681
}