utils.dart 17.4 KB
Newer Older
1 2 3 4 5 6 7
// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// 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 'adb.dart';

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

List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
22
ProcessManager _processManager = const LocalProcessManager();
23 24 25 26

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

27
  final DateTime startTime = DateTime.now();
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
  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() {
55
    final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
    if (details != null && details.trim().isNotEmpty) {
      buf.writeln();
      // Indent details by 4 spaces
      for (String line in details.trim().split('\n')) {
        buf.writeln('    $line');
      }
    }
    return '$buf';
  }
}

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

  final String message;

  @override
  String toString() => message;
}

void fail(String message) {
77
  throw BuildFailedError(message);
78 79
}

80 81 82 83 84 85 86 87 88 89 90 91
// 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');
    }
  }
92 93 94 95
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
96
  rm(entity, recursive: true);
97 98 99 100
}

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

101
Directory dir(String path) => Directory(path);
102

103
File file(String path) => File(path);
104 105

void copy(File sourceFile, Directory targetDirectory, {String name}) {
106
  final File target = file(
107 108 109 110
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

111 112 113 114 115 116 117
void recursiveCopy(Directory source, Directory target) {
  if (!target.existsSync())
    target.createSync();

  for (FileSystemEntity entity in source.listSync(followLinks: false)) {
    final String name = path.basename(entity.path);
    if (entity is Directory)
118
      recursiveCopy(entity, Directory(path.join(target.path, name)));
119
    else if (entity is File) {
120
      final File dest = File(path.join(target.path, name));
121 122 123 124 125
      dest.writeAsBytesSync(entity.readAsBytesSync());
    }
  }
}

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
FileSystemEntity move(FileSystemEntity whatToMove,
    {Directory to, String name}) {
  return whatToMove
      .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}

/// 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) {
145 146 147 148 149 150
  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');
151 152 153 154
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
155
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
156 157 158 159 160 161
  String version = result.stderr.trim();

  // 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
162
  if (version.contains('('))
163
    version = version.substring(0, version.indexOf('(')).trim();
164
  if (version.contains(':'))
165 166 167 168 169 170 171
    version = version.substring(version.indexOf(':') + 1).trim();

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

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

175
  return inDirectory<String>(flutterDirectory, () {
176 177 178 179 180 181
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
  // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
182
  return inDirectory<DateTime>(flutterDirectory, () async {
183
    final String unixTimestamp = await eval('git', <String>[
184 185 186 187 188
      'show',
      '-s',
      '--format=%at',
      commit,
    ]);
189
    final int secondsSinceEpoch = int.parse(unixTimestamp);
190
    return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
191 192 193
  });
}

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
/// 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.
218 219 220 221
Future<Process> startProcess(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
222
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
223 224
  String workingDirectory,
}) async {
225
  assert(isBot != null);
226
  final String command = '$executable ${arguments?.join(" ") ?? ""}';
227
  print('\nExecuting: $command');
228
  environment ??= <String, String>{};
229
  environment['BOT'] = isBot ? 'true' : 'false';
230 231
  final Process process = await _processManager.start(
    <String>[executable]..addAll(arguments),
232 233 234
    environment: environment,
    workingDirectory: workingDirectory ?? cwd,
  );
235
  final ProcessInfo processInfo = ProcessInfo(command, process);
236 237
  _runningProcesses.add(processInfo);

238
  process.exitCode.then<void>((int exitCode) {
239
    print('"$executable" exit code: $exitCode');
240
    _runningProcesses.remove(processInfo);
241 242
  });

243
  return process;
244 245
}

246
Future<void> forceQuitRunningProcesses() async {
247 248 249 250
  if (_runningProcesses.isEmpty)
    return;

  // Give normally quitting processes a chance to report their exit code.
251
  await Future<void>.delayed(const Duration(seconds: 1));
252 253 254

  // Whatever's left, kill it.
  for (ProcessInfo p in _runningProcesses) {
255
    print('Force-quitting process:\n$p');
256 257 258 259 260 261 262 263
    if (!p.process.kill()) {
      print('Failed to force quit process');
    }
  }
  _runningProcesses.clear();
}

/// Executes a command and returns its exit code.
264 265 266 267
Future<int> exec(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
268
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
269
  String workingDirectory,
270
}) async {
271
  final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory);
272

273 274
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
275
  process.stdout
276 277
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
278 279 280
      .listen((String line) {
        print('stdout: $line');
      }, onDone: () { stdoutDone.complete(); });
281
  process.stderr
282 283
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
284 285 286
      .listen((String line) {
        print('stderr: $line');
      }, onDone: () { stderrDone.complete(); });
287

288
  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
289
  final int exitCode = await process.exitCode;
290 291

  if (exitCode != 0 && !canFail)
292
    fail('Executable "$executable" failed with exit code $exitCode.');
293 294 295 296 297 298

  return exitCode;
}

/// Executes a command and returns its standard output as a String.
///
299
/// For logging purposes, the command's output is also printed out.
300 301 302 303
Future<String> eval(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
304
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
305
  String workingDirectory,
306
}) async {
307
  final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory);
308

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

326
  await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
327
  final int exitCode = await process.exitCode;
328 329

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

332
  return output.toString().trimRight();
333 334
}

335
Future<int> flutter(String command, {
336
  List<String> options = const <String>[],
337
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
338 339
  Map<String, String> environment,
}) {
340
  final List<String> args = <String>[command]..addAll(options);
341
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
342
      canFail: canFail, environment: environment);
343 344
}

345
/// Runs a `flutter` command and returns the standard output as a string.
346
Future<String> evalFlutter(String command, {
347
  List<String> options = const <String>[],
348
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
349 350
  Map<String, String> environment,
}) {
351
  final List<String> args = <String>[command]..addAll(options);
352
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
353
      canFail: canFail, environment: environment);
354 355
}

356 357 358 359 360
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

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

361 362 363 364 365 366 367 368 369 370 371 372 373 374
/// 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));
}

375
Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async {
376
  final String previousCwd = cwd;
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
  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';
}

Directory get flutterDirectory => dir('../..').absolute;

String requireEnvVar(String name) {
404
  final String value = Platform.environment[name];
405

406 407
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
408 409 410 411

  return value;
}

412
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
413 414
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
415
  final T result = map[propertyName];
416
  return result;
417 418 419
}

String jsonEncode(dynamic data) {
420
  return const JsonEncoder.withIndent('  ').convert(data) + '\n';
421 422
}

423
Future<void> getFlutter(String revision) async {
424 425 426
  section('Get Flutter!');

  if (exists(flutterDirectory)) {
427
    flutterDirectory.deleteSync(recursive: true);
428 429
  }

430
  await inDirectory<void>(flutterDirectory.parent, () async {
431 432 433
    await exec('git', <String>['clone', 'https://github.com/flutter/flutter.git']);
  });

434
  await inDirectory<void>(flutterDirectory, () async {
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
    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) {
///
///     }
498 499
Future<void> runAndCaptureAsyncStacks(Future<void> callback()) {
  final Completer<void> completer = Completer<void>();
500 501 502
  Chain.capture(() async {
    await callback();
    completer.complete();
503
  }, onError: completer.completeError);
504 505
  return completer.future;
}
506

507
bool canRun(String path) => _processManager.canRun(path);
508 509

String extractCloudAuthTokenArg(List<String> rawArgs) {
510
  final ArgParser argParser = ArgParser()..addOption('cloud-auth-token');
511 512 513
  ArgResults args;
  try {
    args = argParser.parse(rawArgs);
514
  } on FormatException catch (error) {
515 516 517 518 519 520 521 522 523 524 525 526 527
    stderr.writeln('${error.message}\n');
    stderr.writeln('Usage:\n');
    stderr.writeln(argParser.usage);
    return null;
  }

  final String token = args['cloud-auth-token'];
  if (token == null) {
    stderr.writeln('Required option --cloud-auth-token not found');
    return null;
  }
  return token;
}
528

529 530 531 532 533 534 535 536 537 538
/// Tries to extract a port from the string.
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
///
/// The `multiLine` flag should be set to true if `line` is actually a buffer of many lines.
int parseServicePort(String line, {
  String prefix = 'An Observatory debugger .* is available at: ',
  bool multiLine = false,
}) {
  // e.g. "An Observatory debugger and profiler on ... is available at: http://127.0.0.1:8100/"
539
  final RegExp pattern = RegExp('$prefix(\\S+:(\\d+)/\\S*)\$', multiLine: multiLine);
540
  final Match match = pattern.firstMatch(line);
541 542
  return match == null ? null : int.parse(match.group(2));
}
543 544 545 546 547 548 549 550 551

/// If FLUTTER_ENGINE environment variable is set then we need to pass
/// correct --local-engine setting too.
void setLocalEngineOptionIfNecessary(List<String> options, [String flavor]) {
  if (Platform.environment['FLUTTER_ENGINE'] != null) {
    if (flavor == null) {
      // If engine flavor was not specified explicitly then scan options looking
      // for flags that specify the engine flavor (--release, --profile or
      // --debug). Default flavor to debug if no flags were found.
552
      const Map<String, String> optionToFlavor = <String, String>{
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
        '--release': 'release',
        '--debug': 'debug',
        '--profile': 'profile',
      };

      for (String option in options) {
        flavor = optionToFlavor[option];
        if (flavor != null) {
          break;
        }
      }

      flavor ??= 'debug';
    }

568
    const Map<DeviceOperatingSystem, String> osNames = <DeviceOperatingSystem, String>{
569 570 571 572 573 574 575
      DeviceOperatingSystem.ios: 'ios',
      DeviceOperatingSystem.android: 'android',
    };

    options.add('--local-engine=${osNames[deviceOperatingSystem]}_$flavor');
  }
}