utils.dart 16.6 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 27 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

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

  final DateTime startTime = new DateTime.now();
  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 = new 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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
    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) {
  throw new BuildFailedError(message);
}

void rm(FileSystemEntity entity) {
  if (entity.existsSync())
    entity.deleteSync();
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
  if (entity.existsSync())
    entity.deleteSync(recursive: true);
}

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

Directory dir(String path) => new Directory(path);

File file(String path) => new File(path);

void copy(File sourceFile, Directory targetDirectory, {String name}) {
98
  final File target = file(
99 100 101 102
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

103 104 105 106 107 108 109 110 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)
      recursiveCopy(entity, new Directory(path.join(target.path, name)));
    else if (entity is File) {
      final File dest = new File(path.join(target.path, name));
      dest.writeAsBytesSync(entity.readAsBytesSync());
    }
  }
}

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
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) {
137 138 139 140 141 142
  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');
143 144 145 146
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
147
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
148 149 150 151 152 153
  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
154
  if (version.contains('('))
155
    version = version.substring(0, version.indexOf('(')).trim();
156
  if (version.contains(':'))
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
    version = version.substring(version.indexOf(':') + 1).trim();

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

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

  return inDirectory(flutterDirectory, () {
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
  // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
  return inDirectory(flutterDirectory, () async {
175
    final String unixTimestamp = await eval('git', <String>[
176 177 178 179 180
      'show',
      '-s',
      '--format=%at',
      commit,
    ]);
181
    final int secondsSinceEpoch = int.parse(unixTimestamp);
182 183 184 185
    return new DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
  });
}

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

230 231
  process.exitCode.then((int exitCode) {
    print('exitcode: $exitCode');
232
    _runningProcesses.remove(processInfo);
233 234
  });

235
  return process;
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
}

Future<Null> forceQuitRunningProcesses() async {
  if (_runningProcesses.isEmpty)
    return;

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

  // Whatever's left, kill it.
  for (ProcessInfo p in _runningProcesses) {
    print('Force quitting process:\n$p');
    if (!p.process.kill()) {
      print('Failed to force quit process');
    }
  }
  _runningProcesses.clear();
}

/// Executes a command and returns its exit code.
256 257 258 259
Future<int> exec(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
260
  bool canFail = false,
261
}) async {
262
  final Process process = await startProcess(executable, arguments, environment: environment);
263

264 265
  final Completer<Null> stdoutDone = new Completer<Null>();
  final Completer<Null> stderrDone = new Completer<Null>();
266
  process.stdout
267
      .transform(utf8.decoder)
268
      .transform(const LineSplitter())
269 270 271
      .listen((String line) {
        print('stdout: $line');
      }, onDone: () { stdoutDone.complete(); });
272
  process.stderr
273
      .transform(utf8.decoder)
274
      .transform(const LineSplitter())
275 276 277
      .listen((String line) {
        print('stderr: $line');
      }, onDone: () { stderrDone.complete(); });
278

279
  await Future.wait<Null>(<Future<Null>>[stdoutDone.future, stderrDone.future]);
280
  final int exitCode = await process.exitCode;
281 282 283 284 285 286 287 288 289

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

  return exitCode;
}

/// Executes a command and returns its standard output as a String.
///
290
/// For logging purposes, the command's output is also printed out.
291 292 293 294
Future<String> eval(
  String executable,
  List<String> arguments, {
  Map<String, String> environment,
295
  bool canFail = false,
296
}) async {
297
  final Process process = await startProcess(executable, arguments, environment: environment);
298 299 300 301 302

  final StringBuffer output = new StringBuffer();
  final Completer<Null> stdoutDone = new Completer<Null>();
  final Completer<Null> stderrDone = new Completer<Null>();
  process.stdout
303
      .transform(utf8.decoder)
304 305 306 307 308 309
      .transform(const LineSplitter())
      .listen((String line) {
        print('stdout: $line');
        output.writeln(line);
      }, onDone: () { stdoutDone.complete(); });
  process.stderr
310
      .transform(utf8.decoder)
311 312 313 314 315 316
      .transform(const LineSplitter())
      .listen((String line) {
        print('stderr: $line');
      }, onDone: () { stderrDone.complete(); });

  await Future.wait<Null>(<Future<Null>>[stdoutDone.future, stderrDone.future]);
317
  final int exitCode = await process.exitCode;
318 319 320 321

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

322
  return output.toString().trimRight();
323 324
}

325
Future<int> flutter(String command, {
326 327
  List<String> options = const <String>[],
  bool canFail = false,
328 329
  Map<String, String> environment,
}) {
330
  final List<String> args = <String>[command]..addAll(options);
331
  return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
332
      canFail: canFail, environment: environment);
333 334
}

335
/// Runs a `flutter` command and returns the standard output as a string.
336
Future<String> evalFlutter(String command, {
337 338
  List<String> options = const <String>[],
  bool canFail = false,
339 340
  Map<String, String> environment,
}) {
341
  final List<String> args = <String>[command]..addAll(options);
342
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
343
      canFail: canFail, environment: environment);
344 345
}

346 347 348 349 350
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

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

351 352 353 354 355 356 357 358 359 360 361 362 363 364
/// 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));
}

365
Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async {
366
  final String previousCwd = cwd;
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
  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) {
394
  final String value = Platform.environment[name];
395

396 397
  if (value == null)
    fail('$name environment variable is missing. Quitting.');
398 399 400 401

  return value;
}

402
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
403 404
  if (!map.containsKey(propertyName))
    fail('Configuration property not found: $propertyName');
405
  final T result = map[propertyName];
406
  return result;
407 408 409
}

String jsonEncode(dynamic data) {
410
  return const JsonEncoder.withIndent('  ').convert(data) + '\n';
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 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
}

Future<Null> getFlutter(String revision) async {
  section('Get Flutter!');

  if (exists(flutterDirectory)) {
    rmTree(flutterDirectory);
  }

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

  await inDirectory(flutterDirectory, () async {
    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) {
///
///     }
Future<Null> runAndCaptureAsyncStacks(Future<Null> callback()) {
489
  final Completer<Null> completer = new Completer<Null>();
490 491 492
  Chain.capture(() async {
    await callback();
    completer.complete();
493
  }, onError: completer.completeError);
494 495
  return completer.future;
}
496

497
bool canRun(String path) => _processManager.canRun(path);
498 499 500 501 502 503

String extractCloudAuthTokenArg(List<String> rawArgs) {
  final ArgParser argParser = new ArgParser()..addOption('cloud-auth-token');
  ArgResults args;
  try {
    args = argParser.parse(rawArgs);
504
  } on FormatException catch (error) {
505 506 507 508 509 510 511 512 513 514 515 516 517
    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;
}
518

519 520 521 522 523 524 525 526 527 528 529 530 531 532
/// 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/"
  final RegExp pattern = new RegExp('$prefix(\\S+:(\\d+)/\\S*)\$', multiLine: multiLine);
  final Match match = pattern.firstMatch(line);
  print(pattern);
  print(match);
533 534
  return match == null ? null : int.parse(match.group(2));
}
535 536 537 538 539 540 541 542 543

/// 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.
544
      const Map<String, String> optionToFlavor = <String, String>{
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
        '--release': 'release',
        '--debug': 'debug',
        '--profile': 'profile',
      };

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

      flavor ??= 'debug';
    }

560
    const Map<DeviceOperatingSystem, String> osNames = <DeviceOperatingSystem, String>{
561 562 563 564 565 566 567
      DeviceOperatingSystem.ios: 'ios',
      DeviceOperatingSystem.android: 'android',
    };

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