1// Copyright (c) 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:convert'; 7import 'dart:io'; 8import 'dart:math' as math; 9 10import 'package:args/args.dart'; 11import 'package:meta/meta.dart'; 12import 'package:path/path.dart' as path; 13import 'package:process/process.dart'; 14import 'package:stack_trace/stack_trace.dart'; 15 16/// Virtual current working directory, which affect functions, such as [exec]. 17String cwd = Directory.current.path; 18 19/// The local engine to use for [flutter] and [evalFlutter], if any. 20String get localEngine => const String.fromEnvironment('localEngine'); 21 22/// The local engine source path to use if a local engine is used for [flutter] 23/// and [evalFlutter]. 24String get localEngineSrcPath => const String.fromEnvironment('localEngineSrcPath'); 25 26List<ProcessInfo> _runningProcesses = <ProcessInfo>[]; 27ProcessManager _processManager = const LocalProcessManager(); 28 29class ProcessInfo { 30 ProcessInfo(this.command, this.process); 31 32 final DateTime startTime = DateTime.now(); 33 final String command; 34 final Process process; 35 36 @override 37 String toString() { 38 return ''' 39 command : $command 40 started : $startTime 41 pid : ${process.pid} 42''' 43 .trim(); 44 } 45} 46 47/// Result of a health check for a specific parameter. 48class HealthCheckResult { 49 HealthCheckResult.success([this.details]) : succeeded = true; 50 HealthCheckResult.failure(this.details) : succeeded = false; 51 HealthCheckResult.error(dynamic error, dynamic stackTrace) 52 : succeeded = false, 53 details = 'ERROR: $error${'\n$stackTrace' ?? ''}'; 54 55 final bool succeeded; 56 final String details; 57 58 @override 59 String toString() { 60 final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed'); 61 if (details != null && details.trim().isNotEmpty) { 62 buf.writeln(); 63 // Indent details by 4 spaces 64 for (String line in details.trim().split('\n')) { 65 buf.writeln(' $line'); 66 } 67 } 68 return '$buf'; 69 } 70} 71 72class BuildFailedError extends Error { 73 BuildFailedError(this.message); 74 75 final String message; 76 77 @override 78 String toString() => message; 79} 80 81void fail(String message) { 82 throw BuildFailedError(message); 83} 84 85// Remove the given file or directory. 86void rm(FileSystemEntity entity, { bool recursive = false}) { 87 if (entity.existsSync()) { 88 // This should not be necessary, but it turns out that 89 // on Windows it's common for deletions to fail due to 90 // bogus (we think) "access denied" errors. 91 try { 92 entity.deleteSync(recursive: recursive); 93 } on FileSystemException catch (error) { 94 print('Failed to delete ${entity.path}: $error'); 95 } 96 } 97} 98 99/// Remove recursively. 100void rmTree(FileSystemEntity entity) { 101 rm(entity, recursive: true); 102} 103 104List<FileSystemEntity> ls(Directory directory) => directory.listSync(); 105 106Directory dir(String path) => Directory(path); 107 108File file(String path) => File(path); 109 110void copy(File sourceFile, Directory targetDirectory, {String name}) { 111 final File target = file( 112 path.join(targetDirectory.path, name ?? path.basename(sourceFile.path))); 113 target.writeAsBytesSync(sourceFile.readAsBytesSync()); 114} 115 116void recursiveCopy(Directory source, Directory target) { 117 if (!target.existsSync()) 118 target.createSync(); 119 120 for (FileSystemEntity entity in source.listSync(followLinks: false)) { 121 final String name = path.basename(entity.path); 122 if (entity is Directory) 123 recursiveCopy(entity, Directory(path.join(target.path, name))); 124 else if (entity is File) { 125 final File dest = File(path.join(target.path, name)); 126 dest.writeAsBytesSync(entity.readAsBytesSync()); 127 } 128 } 129} 130 131FileSystemEntity move(FileSystemEntity whatToMove, 132 {Directory to, String name}) { 133 return whatToMove 134 .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path))); 135} 136 137/// Equivalent of `mkdir directory`. 138void mkdir(Directory directory) { 139 directory.createSync(); 140} 141 142/// Equivalent of `mkdir -p directory`. 143void mkdirs(Directory directory) { 144 directory.createSync(recursive: true); 145} 146 147bool exists(FileSystemEntity entity) => entity.existsSync(); 148 149void section(String title) { 150 title = '╡ ••• $title ••• ╞'; 151 final String line = '═' * math.max((80 - title.length) ~/ 2, 2); 152 String output = '$line$title$line'; 153 if (output.length == 79) 154 output += '═'; 155 print('\n\n$output\n'); 156} 157 158Future<String> getDartVersion() async { 159 // The Dart VM returns the version text to stderr. 160 final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']); 161 String version = result.stderr.trim(); 162 163 // Convert: 164 // Dart VM version: 1.17.0-dev.2.0 (Tue May 3 12:14:52 2016) on "macos_x64" 165 // to: 166 // 1.17.0-dev.2.0 167 if (version.contains('(')) 168 version = version.substring(0, version.indexOf('(')).trim(); 169 if (version.contains(':')) 170 version = version.substring(version.indexOf(':') + 1).trim(); 171 172 return version.replaceAll('"', "'"); 173} 174 175Future<String> getCurrentFlutterRepoCommit() { 176 if (!dir('${flutterDirectory.path}/.git').existsSync()) { 177 return Future<String>.value(null); 178 } 179 180 return inDirectory<String>(flutterDirectory, () { 181 return eval('git', <String>['rev-parse', 'HEAD']); 182 }); 183} 184 185Future<DateTime> getFlutterRepoCommitTimestamp(String commit) { 186 // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65 187 return inDirectory<DateTime>(flutterDirectory, () async { 188 final String unixTimestamp = await eval('git', <String>[ 189 'show', 190 '-s', 191 '--format=%at', 192 commit, 193 ]); 194 final int secondsSinceEpoch = int.parse(unixTimestamp); 195 return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000); 196 }); 197} 198 199/// Starts a subprocess. 200/// 201/// The first argument is the full path to the executable to run. 202/// 203/// The second argument is the list of arguments to provide on the command line. 204/// This argument can be null, indicating no arguments (same as the empty list). 205/// 206/// The `environment` argument can be provided to configure environment variables 207/// that will be made available to the subprocess. The `BOT` environment variable 208/// is always set and overrides any value provided in the `environment` argument. 209/// The `isBot` argument controls the value of the `BOT` variable. It will either 210/// be "true", if `isBot` is true (the default), or "false" if it is false. 211/// 212/// The `BOT` variable is in particular used by the `flutter` tool to determine 213/// how verbose to be and whether to enable analytics by default. 214/// 215/// The working directory can be provided using the `workingDirectory` argument. 216/// By default it will default to the current working directory (see [cwd]). 217/// 218/// Information regarding the execution of the subprocess is printed to the 219/// console. 220/// 221/// The actual process executes asynchronously. A handle to the subprocess is 222/// returned in the form of a [Future] that completes to a [Process] object. 223Future<Process> startProcess( 224 String executable, 225 List<String> arguments, { 226 Map<String, String> environment, 227 bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs) 228 String workingDirectory, 229}) async { 230 assert(isBot != null); 231 final String command = '$executable ${arguments?.join(" ") ?? ""}'; 232 print('\nExecuting: $command'); 233 environment ??= <String, String>{}; 234 environment['BOT'] = isBot ? 'true' : 'false'; 235 final Process process = await _processManager.start( 236 <String>[executable, ...arguments], 237 environment: environment, 238 workingDirectory: workingDirectory ?? cwd, 239 ); 240 final ProcessInfo processInfo = ProcessInfo(command, process); 241 _runningProcesses.add(processInfo); 242 243 process.exitCode.then<void>((int exitCode) { 244 print('"$executable" exit code: $exitCode'); 245 _runningProcesses.remove(processInfo); 246 }); 247 248 return process; 249} 250 251Future<void> forceQuitRunningProcesses() async { 252 if (_runningProcesses.isEmpty) 253 return; 254 255 // Give normally quitting processes a chance to report their exit code. 256 await Future<void>.delayed(const Duration(seconds: 1)); 257 258 // Whatever's left, kill it. 259 for (ProcessInfo p in _runningProcesses) { 260 print('Force-quitting process:\n$p'); 261 if (!p.process.kill()) { 262 print('Failed to force quit process'); 263 } 264 } 265 _runningProcesses.clear(); 266} 267 268/// Executes a command and returns its exit code. 269Future<int> exec( 270 String executable, 271 List<String> arguments, { 272 Map<String, String> environment, 273 bool canFail = false, // as in, whether failures are ok. False means that they are fatal. 274 String workingDirectory, 275}) async { 276 final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory); 277 278 final Completer<void> stdoutDone = Completer<void>(); 279 final Completer<void> stderrDone = Completer<void>(); 280 process.stdout 281 .transform<String>(utf8.decoder) 282 .transform<String>(const LineSplitter()) 283 .listen((String line) { 284 print('stdout: $line'); 285 }, onDone: () { stdoutDone.complete(); }); 286 process.stderr 287 .transform<String>(utf8.decoder) 288 .transform<String>(const LineSplitter()) 289 .listen((String line) { 290 print('stderr: $line'); 291 }, onDone: () { stderrDone.complete(); }); 292 293 await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]); 294 final int exitCode = await process.exitCode; 295 296 if (exitCode != 0 && !canFail) 297 fail('Executable "$executable" failed with exit code $exitCode.'); 298 299 return exitCode; 300} 301 302/// Executes a command and returns its standard output as a String. 303/// 304/// For logging purposes, the command's output is also printed out by default. 305Future<String> eval( 306 String executable, 307 List<String> arguments, { 308 Map<String, String> environment, 309 bool canFail = false, // as in, whether failures are ok. False means that they are fatal. 310 String workingDirectory, 311 StringBuffer stderr, // if not null, the stderr will be written here 312 bool printStdout = true, 313 bool printStderr = true, 314}) async { 315 final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory); 316 317 final StringBuffer output = StringBuffer(); 318 final Completer<void> stdoutDone = Completer<void>(); 319 final Completer<void> stderrDone = Completer<void>(); 320 process.stdout 321 .transform<String>(utf8.decoder) 322 .transform<String>(const LineSplitter()) 323 .listen((String line) { 324 if (printStdout) { 325 print('stdout: $line'); 326 } 327 output.writeln(line); 328 }, onDone: () { stdoutDone.complete(); }); 329 process.stderr 330 .transform<String>(utf8.decoder) 331 .transform<String>(const LineSplitter()) 332 .listen((String line) { 333 if (printStderr) { 334 print('stderr: $line'); 335 } 336 stderr?.writeln(line); 337 }, onDone: () { stderrDone.complete(); }); 338 339 await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]); 340 final int exitCode = await process.exitCode; 341 342 if (exitCode != 0 && !canFail) 343 fail('Executable "$executable" failed with exit code $exitCode.'); 344 345 return output.toString().trimRight(); 346} 347 348List<String> flutterCommandArgs(String command, List<String> options) { 349 return <String>[ 350 command, 351 if (localEngine != null) ...<String>['--local-engine', localEngine], 352 if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath], 353 ...options, 354 ]; 355} 356 357Future<int> flutter(String command, { 358 List<String> options = const <String>[], 359 bool canFail = false, // as in, whether failures are ok. False means that they are fatal. 360 Map<String, String> environment, 361}) { 362 final List<String> args = flutterCommandArgs(command, options); 363 return exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args, 364 canFail: canFail, environment: environment); 365} 366 367/// Runs a `flutter` command and returns the standard output as a string. 368Future<String> evalFlutter(String command, { 369 List<String> options = const <String>[], 370 bool canFail = false, // as in, whether failures are ok. False means that they are fatal. 371 Map<String, String> environment, 372 StringBuffer stderr, // if not null, the stderr will be written here. 373}) { 374 final List<String> args = flutterCommandArgs(command, options); 375 return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args, 376 canFail: canFail, environment: environment, stderr: stderr); 377} 378 379String get dartBin => 380 path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); 381 382Future<int> dart(List<String> args) => exec(dartBin, args); 383 384/// Returns a future that completes with a path suitable for JAVA_HOME 385/// or with null, if Java cannot be found. 386Future<String> findJavaHome() async { 387 final Iterable<String> hits = grep( 388 'Java binary at: ', 389 from: await evalFlutter('doctor', options: <String>['-v']), 390 ); 391 if (hits.isEmpty) 392 return null; 393 final String javaBinary = hits.first.split(': ').last; 394 // javaBinary == /some/path/to/java/home/bin/java 395 return path.dirname(path.dirname(javaBinary)); 396} 397 398Future<T> inDirectory<T>(dynamic directory, Future<T> action()) async { 399 final String previousCwd = cwd; 400 try { 401 cd(directory); 402 return await action(); 403 } finally { 404 cd(previousCwd); 405 } 406} 407 408void cd(dynamic directory) { 409 Directory d; 410 if (directory is String) { 411 cwd = directory; 412 d = dir(directory); 413 } else if (directory is Directory) { 414 cwd = directory.path; 415 d = directory; 416 } else { 417 throw 'Unsupported type ${directory.runtimeType} of $directory'; 418 } 419 420 if (!d.existsSync()) 421 throw 'Cannot cd into directory that does not exist: $directory'; 422} 423 424Directory get flutterDirectory => dir('../..').absolute; 425 426String requireEnvVar(String name) { 427 final String value = Platform.environment[name]; 428 429 if (value == null) 430 fail('$name environment variable is missing. Quitting.'); 431 432 return value; 433} 434 435T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) { 436 if (!map.containsKey(propertyName)) 437 fail('Configuration property not found: $propertyName'); 438 final T result = map[propertyName]; 439 return result; 440} 441 442String jsonEncode(dynamic data) { 443 return const JsonEncoder.withIndent(' ').convert(data) + '\n'; 444} 445 446Future<void> getFlutter(String revision) async { 447 section('Get Flutter!'); 448 449 if (exists(flutterDirectory)) { 450 flutterDirectory.deleteSync(recursive: true); 451 } 452 453 await inDirectory<void>(flutterDirectory.parent, () async { 454 await exec('git', <String>['clone', 'https://github.com/flutter/flutter.git']); 455 }); 456 457 await inDirectory<void>(flutterDirectory, () async { 458 await exec('git', <String>['checkout', revision]); 459 }); 460 461 await flutter('config', options: <String>['--no-analytics']); 462 463 section('flutter doctor'); 464 await flutter('doctor'); 465 466 section('flutter update-packages'); 467 await flutter('update-packages'); 468} 469 470void checkNotNull(Object o1, 471 [Object o2 = 1, 472 Object o3 = 1, 473 Object o4 = 1, 474 Object o5 = 1, 475 Object o6 = 1, 476 Object o7 = 1, 477 Object o8 = 1, 478 Object o9 = 1, 479 Object o10 = 1]) { 480 if (o1 == null) 481 throw 'o1 is null'; 482 if (o2 == null) 483 throw 'o2 is null'; 484 if (o3 == null) 485 throw 'o3 is null'; 486 if (o4 == null) 487 throw 'o4 is null'; 488 if (o5 == null) 489 throw 'o5 is null'; 490 if (o6 == null) 491 throw 'o6 is null'; 492 if (o7 == null) 493 throw 'o7 is null'; 494 if (o8 == null) 495 throw 'o8 is null'; 496 if (o9 == null) 497 throw 'o9 is null'; 498 if (o10 == null) 499 throw 'o10 is null'; 500} 501 502/// Splits [from] into lines and selects those that contain [pattern]. 503Iterable<String> grep(Pattern pattern, {@required String from}) { 504 return from.split('\n').where((String line) { 505 return line.contains(pattern); 506 }); 507} 508 509/// Captures asynchronous stack traces thrown by [callback]. 510/// 511/// This is a convenience wrapper around [Chain] optimized for use with 512/// `async`/`await`. 513/// 514/// Example: 515/// 516/// try { 517/// await captureAsyncStacks(() { /* async things */ }); 518/// } catch (error, chain) { 519/// 520/// } 521Future<void> runAndCaptureAsyncStacks(Future<void> callback()) { 522 final Completer<void> completer = Completer<void>(); 523 Chain.capture(() async { 524 await callback(); 525 completer.complete(); 526 }, onError: completer.completeError); 527 return completer.future; 528} 529 530bool canRun(String path) => _processManager.canRun(path); 531 532String extractCloudAuthTokenArg(List<String> rawArgs) { 533 final ArgParser argParser = ArgParser()..addOption('cloud-auth-token'); 534 ArgResults args; 535 try { 536 args = argParser.parse(rawArgs); 537 } on FormatException catch (error) { 538 stderr.writeln('${error.message}\n'); 539 stderr.writeln('Usage:\n'); 540 stderr.writeln(argParser.usage); 541 return null; 542 } 543 544 final String token = args['cloud-auth-token']; 545 if (token == null) { 546 stderr.writeln('Required option --cloud-auth-token not found'); 547 return null; 548 } 549 return token; 550} 551 552final RegExp _obsRegExp = 553 RegExp('An Observatory debugger .* is available at: '); 554final RegExp _obsPortRegExp = RegExp('(\\S+:(\\d+)/\\S*)\$'); 555final RegExp _obsUriRegExp = RegExp('((http|\/\/)[a-zA-Z0-9:/=_\\-\.\\[\\]]+)'); 556 557/// Tries to extract a port from the string. 558/// 559/// The `prefix`, if specified, is a regular expression pattern and must not contain groups. 560/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `. 561int parseServicePort(String line, { 562 Pattern prefix, 563}) { 564 prefix ??= _obsRegExp; 565 final Iterable<Match> matchesIter = prefix.allMatches(line); 566 if (matchesIter.isEmpty) { 567 return null; 568 } 569 final Match prefixMatch = matchesIter.first; 570 final List<Match> matches = 571 _obsPortRegExp.allMatches(line, prefixMatch.end).toList(); 572 return matches.isEmpty ? null : int.parse(matches[0].group(2)); 573} 574 575/// Tries to extract a Uri from the string. 576/// 577/// The `prefix`, if specified, is a regular expression pattern and must not contain groups. 578/// `prefix` defaults to the RegExp: `An Observatory debugger .* is available at: `. 579Uri parseServiceUri(String line, { 580 Pattern prefix, 581}) { 582 prefix ??= _obsRegExp; 583 final Iterable<Match> matchesIter = prefix.allMatches(line); 584 if (matchesIter.isEmpty) { 585 return null; 586 } 587 final Match prefixMatch = matchesIter.first; 588 final List<Match> matches = 589 _obsUriRegExp.allMatches(line, prefixMatch.end).toList(); 590 return matches.isEmpty ? null : Uri.parse(matches[0].group(0)); 591} 592 593/// Checks that the file exists, otherwise throws a [FileSystemException]. 594void checkFileExists(String file) { 595 if (!exists(File(file))) { 596 throw FileSystemException('Expected file to exit.', file); 597 } 598} 599