• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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