• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 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';
6
7import 'package:args/args.dart';
8import 'package:args/command_runner.dart';
9import 'package:completion/completion.dart';
10import 'package:file/file.dart';
11import 'package:platform/platform.dart';
12import 'package:process/process.dart';
13
14import '../artifacts.dart';
15import '../base/common.dart';
16import '../base/context.dart';
17import '../base/file_system.dart';
18import '../base/flags.dart';
19import '../base/io.dart' as io;
20import '../base/logger.dart';
21import '../base/os.dart';
22import '../base/platform.dart';
23import '../base/process.dart';
24import '../base/process_manager.dart';
25import '../base/terminal.dart';
26import '../base/user_messages.dart';
27import '../base/utils.dart';
28import '../cache.dart';
29import '../convert.dart';
30import '../dart/package_map.dart';
31import '../device.dart';
32import '../globals.dart';
33import '../reporting/reporting.dart';
34import '../tester/flutter_tester.dart';
35import '../version.dart';
36import '../vmservice.dart';
37
38const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
39const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
40const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/
41const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/
42const String kFlutterEnginePackageName = 'sky_engine';
43
44class FlutterCommandRunner extends CommandRunner<void> {
45  FlutterCommandRunner({ bool verboseHelp = false }) : super(
46    'flutter',
47    'Manage your Flutter app development.\n'
48      '\n'
49      'Common commands:\n'
50      '\n'
51      '  flutter create <output directory>\n'
52      '    Create a new Flutter project in the specified directory.\n'
53      '\n'
54      '  flutter run [options]\n'
55      '    Run your Flutter application on an attached device or in an emulator.',
56  ) {
57    argParser.addFlag('verbose',
58        abbr: 'v',
59        negatable: false,
60        help: 'Noisy logging, including all shell commands executed.\n'
61              'If used with --help, shows hidden options.');
62    argParser.addFlag('quiet',
63        negatable: false,
64        hide: !verboseHelp,
65        help: 'Reduce the amount of output from some commands.');
66    argParser.addFlag('wrap',
67        negatable: true,
68        hide: !verboseHelp,
69        help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
70        defaultsTo: true);
71    argParser.addOption('wrap-column',
72        hide: !verboseHelp,
73        help: 'Sets the output wrap column. If not set, uses the width of the terminal. No '
74            'wrapping occurs if not writing to a terminal. Use --no-wrap to turn off wrapping '
75            'when connected to a terminal.',
76        defaultsTo: null);
77    argParser.addOption('device-id',
78        abbr: 'd',
79        help: 'Target device id or name (prefixes allowed).');
80    argParser.addFlag('version',
81        negatable: false,
82        help: 'Reports the version of this tool.');
83    argParser.addFlag('machine',
84        negatable: false,
85        hide: !verboseHelp,
86        help: 'When used with the --version flag, outputs the information using JSON.');
87    argParser.addFlag('color',
88        negatable: true,
89        hide: !verboseHelp,
90        help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
91        defaultsTo: true);
92    argParser.addFlag('version-check',
93        negatable: true,
94        defaultsTo: true,
95        hide: !verboseHelp,
96        help: 'Allow Flutter to check for updates when this command runs.');
97    argParser.addFlag('suppress-analytics',
98        negatable: false,
99        help: 'Suppress analytics reporting when this command runs.');
100    argParser.addFlag('bug-report',
101        negatable: false,
102        help: 'Captures a bug report file to submit to the Flutter team.\n'
103              'Contains local paths, device identifiers, and log snippets.');
104
105    String packagesHelp;
106    bool showPackagesCommand;
107    if (fs.isFileSync(kPackagesFileName)) {
108      packagesHelp = '(defaults to "$kPackagesFileName")';
109      showPackagesCommand = verboseHelp;
110    } else {
111      packagesHelp = '(required, since the current directory does not contain a "$kPackagesFileName" file)';
112      showPackagesCommand = true;
113    }
114    argParser.addOption('packages',
115        hide: !showPackagesCommand,
116        help: 'Path to your ".packages" file.\n$packagesHelp');
117
118    argParser.addOption('flutter-root',
119        hide: !verboseHelp,
120        help: 'The root directory of the Flutter repository.\n'
121              'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent '
122              'of the directory that the "flutter" script itself is in.');
123
124    if (verboseHelp)
125      argParser.addSeparator('Local build selection options (not normally required):');
126
127    argParser.addOption('local-engine-src-path',
128        hide: !verboseHelp,
129        help: 'Path to your engine src directory, if you are building Flutter locally.\n'
130              'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
131              'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
132              'if any, or, failing that, tries to guess at the location based on the value of the '
133              '--flutter-root option.');
134
135    argParser.addOption('local-engine',
136        hide: !verboseHelp,
137        help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
138              'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
139              'This path is relative to --local-engine-src-path/out.');
140
141    if (verboseHelp)
142      argParser.addSeparator('Options for testing the "flutter" tool itself:');
143
144    argParser.addOption('record-to',
145        hide: !verboseHelp,
146        help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), '
147              'and file system access (reads and writes).\n'
148              'Serializes that recording to a directory with the path specified in this flag. If the '
149              'directory does not already exist, it will be created.');
150    argParser.addOption('replay-from',
151        hide: !verboseHelp,
152        help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from '
153              'the specified recording (obtained via --record-to). The path specified in this flag must refer '
154              'to a directory that holds serialized process invocations structured according to the output of '
155              '--record-to.');
156    argParser.addFlag('show-test-device',
157        negatable: false,
158        hide: !verboseHelp,
159        help: 'List the special \'flutter-tester\' device in device listings. '
160              'This headless device is used to\ntest Flutter tooling.');
161  }
162
163  @override
164  ArgParser get argParser => _argParser;
165  final ArgParser _argParser = ArgParser(
166    allowTrailingOptions: false,
167    usageLineLength: outputPreferences.wrapText ? outputPreferences.wrapColumn : null,
168  );
169
170  @override
171  String get usageFooter {
172    return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.');
173  }
174
175  @override
176  String get usage {
177    final String usageWithoutDescription = super.usage.substring(description.length + 2);
178    return  '${wrapText(description)}\n\n$usageWithoutDescription';
179  }
180
181  static String get defaultFlutterRoot {
182    if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName))
183      return platform.environment[kFlutterRootEnvironmentVariableName];
184    try {
185      if (platform.script.scheme == 'data')
186        return '../..'; // we're running as a test
187
188      if (platform.script.scheme == 'package') {
189        final String packageConfigPath = Uri.parse(platform.packageConfig).toFilePath();
190        return fs.path.dirname(fs.path.dirname(fs.path.dirname(packageConfigPath)));
191      }
192
193      final String script = platform.script.toFilePath();
194      if (fs.path.basename(script) == kSnapshotFileName)
195        return fs.path.dirname(fs.path.dirname(fs.path.dirname(script)));
196      if (fs.path.basename(script) == kFlutterToolsScriptFileName)
197        return fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(script))));
198
199      // If run from a bare script within the repo.
200      if (script.contains('flutter/packages/'))
201        return script.substring(0, script.indexOf('flutter/packages/') + 8);
202      if (script.contains('flutter/examples/'))
203        return script.substring(0, script.indexOf('flutter/examples/') + 8);
204    } catch (error) {
205      // we don't have a logger at the time this is run
206      // (which is why we don't use printTrace here)
207      print(userMessages.runnerNoRoot(error));
208    }
209    return '.';
210  }
211
212  @override
213  ArgResults parse(Iterable<String> args) {
214    try {
215      // This is where the CommandRunner would call argParser.parse(args). We
216      // override this function so we can call tryArgsCompletion instead, so the
217      // completion package can interrogate the argParser, and as part of that,
218      // it calls argParser.parse(args) itself and returns the result.
219      return tryArgsCompletion(args, argParser);
220    } on ArgParserException catch (error) {
221      if (error.commands.isEmpty) {
222        usageException(error.message);
223      }
224
225      Command<void> command = commands[error.commands.first];
226      for (String commandName in error.commands.skip(1)) {
227        command = command.subcommands[commandName];
228      }
229
230      command.usageException(error.message);
231      return null;
232    }
233  }
234
235  @override
236  Future<void> run(Iterable<String> args) {
237    // Have an invocation of 'build' print out it's sub-commands.
238    // TODO(ianh): Move this to the Build command itself somehow.
239    if (args.length == 1 && args.first == 'build')
240      args = <String>['build', '-h'];
241
242    return super.run(args);
243  }
244
245  @override
246  Future<void> runCommand(ArgResults topLevelResults) async {
247    final Map<Type, dynamic> contextOverrides = <Type, dynamic>{
248      Flags: Flags(topLevelResults),
249    };
250
251    // Check for verbose.
252    if (topLevelResults['verbose']) {
253      // Override the logger.
254      contextOverrides[Logger] = VerboseLogger(logger);
255    }
256
257    // Don't set wrapColumns unless the user said to: if it's set, then all
258    // wrapping will occur at this width explicitly, and won't adapt if the
259    // terminal size changes during a run.
260    int wrapColumn;
261    if (topLevelResults.wasParsed('wrap-column')) {
262      try {
263        wrapColumn = int.parse(topLevelResults['wrap-column']);
264        if (wrapColumn < 0) {
265          throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column']));
266        }
267      } on FormatException {
268        throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column']));
269      }
270    }
271
272    // If we're not writing to a terminal with a defined width, then don't wrap
273    // anything, unless the user explicitly said to.
274    final bool useWrapping = topLevelResults.wasParsed('wrap')
275        ? topLevelResults['wrap']
276        : io.stdio.terminalColumns == null ? false : topLevelResults['wrap'];
277    contextOverrides[OutputPreferences] = OutputPreferences(
278      wrapText: useWrapping,
279      showColor: topLevelResults['color'],
280      wrapColumn: wrapColumn,
281    );
282
283    if (topLevelResults['show-test-device'] ||
284        topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
285      FlutterTesterDevices.showFlutterTesterDevice = true;
286    }
287
288    String recordTo = topLevelResults['record-to'];
289    String replayFrom = topLevelResults['replay-from'];
290
291    if (topLevelResults['bug-report']) {
292      // --bug-report implies --record-to=<tmp_path>
293      final Directory tempDir = const LocalFileSystem()
294          .systemTempDirectory
295          .createTempSync('flutter_tools_bug_report.');
296      recordTo = tempDir.path;
297
298      // Record the arguments that were used to invoke this runner.
299      final File manifest = tempDir.childFile('MANIFEST.txt');
300      final StringBuffer buffer = StringBuffer()
301        ..writeln('# arguments')
302        ..writeln(topLevelResults.arguments)
303        ..writeln()
304        ..writeln('# rest')
305        ..writeln(topLevelResults.rest);
306      await manifest.writeAsString(buffer.toString(), flush: true);
307
308      // ZIP the recording up once the recording has been serialized.
309      addShutdownHook(() async {
310        final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip');
311        os.zip(tempDir, zipFile);
312        printStatus(userMessages.runnerBugReportFinished(zipFile.basename));
313      }, ShutdownStage.POST_PROCESS_RECORDING);
314      addShutdownHook(() => tempDir.delete(recursive: true), ShutdownStage.CLEANUP);
315    }
316
317    assert(recordTo == null || replayFrom == null);
318
319    if (recordTo != null) {
320      recordTo = recordTo.trim();
321      if (recordTo.isEmpty)
322        throwToolExit(userMessages.runnerNoRecordTo);
323      contextOverrides.addAll(<Type, dynamic>{
324        ProcessManager: getRecordingProcessManager(recordTo),
325        FileSystem: getRecordingFileSystem(recordTo),
326        Platform: await getRecordingPlatform(recordTo),
327      });
328      VMService.enableRecordingConnection(recordTo);
329    }
330
331    if (replayFrom != null) {
332      replayFrom = replayFrom.trim();
333      if (replayFrom.isEmpty)
334        throwToolExit(userMessages.runnerNoReplayFrom);
335      contextOverrides.addAll(<Type, dynamic>{
336        ProcessManager: await getReplayProcessManager(replayFrom),
337        FileSystem: getReplayFileSystem(replayFrom),
338        Platform: await getReplayPlatform(replayFrom),
339      });
340      VMService.enableReplayConnection(replayFrom);
341    }
342
343    // We must set Cache.flutterRoot early because other features use it (e.g.
344    // enginePath's initializer uses it).
345    final String flutterRoot = topLevelResults['flutter-root'] ?? defaultFlutterRoot;
346    Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
347
348    // Set up the tooling configuration.
349    final String enginePath = _findEnginePath(topLevelResults);
350    if (enginePath != null) {
351      contextOverrides.addAll(<Type, dynamic>{
352        Artifacts: Artifacts.getLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath)),
353      });
354    }
355
356    await context.run<void>(
357      overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
358        return MapEntry<Type, Generator>(type, () => value);
359      }),
360      body: () async {
361        logger.quiet = topLevelResults['quiet'];
362
363        if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
364          await Cache.lock();
365
366        if (topLevelResults['suppress-analytics'])
367          flutterUsage.suppressAnalytics = true;
368
369        _checkFlutterCopy();
370        try {
371          await FlutterVersion.instance.ensureVersionFile();
372        } on FileSystemException catch (e) {
373          printError('Failed to write the version file to the artifact cache: "$e".');
374          printError('Please ensure you have permissions in the artifact cache directory.');
375          throwToolExit('Failed to write the version file');
376        }
377        if (topLevelResults.command?.name != 'upgrade' && topLevelResults['version-check']) {
378          await FlutterVersion.instance.checkFlutterVersionFreshness();
379        }
380
381        if (topLevelResults.wasParsed('packages'))
382          PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages']));
383
384        // See if the user specified a specific device.
385        deviceManager.specifiedDeviceId = topLevelResults['device-id'];
386
387        if (topLevelResults['version']) {
388          flutterUsage.sendCommand('version');
389          String status;
390          if (topLevelResults['machine']) {
391            status = const JsonEncoder.withIndent('  ').convert(FlutterVersion.instance.toJson());
392          } else {
393            status = FlutterVersion.instance.toString();
394          }
395          printStatus(status);
396          return;
397        }
398
399        if (topLevelResults['machine']) {
400          throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
401        }
402        await super.runCommand(topLevelResults);
403      },
404    );
405  }
406
407  String _tryEnginePath(String enginePath) {
408    if (fs.isDirectorySync(fs.path.join(enginePath, 'out')))
409      return enginePath;
410    return null;
411  }
412
413  String _findEnginePath(ArgResults globalResults) {
414    String engineSourcePath = globalResults['local-engine-src-path'] ?? platform.environment[kFlutterEngineEnvironmentVariableName];
415
416    if (engineSourcePath == null && globalResults['local-engine'] != null) {
417      try {
418        Uri engineUri = PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName];
419        // Skip if sky_engine is the self-contained one.
420        if (engineUri != null && fs.identicalSync(fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'pkg', kFlutterEnginePackageName, 'lib'), engineUri.path)) {
421          engineUri = null;
422        }
423        // If sky_engine is specified and the engineSourcePath not set, try to determine the engineSourcePath by sky_engine setting.
424        // A typical engineUri looks like: file://flutter-engine-local-path/src/out/host_debug_unopt/gen/dart-pkg/sky_engine/lib/
425        if (engineUri?.path != null) {
426          engineSourcePath = fs.directory(engineUri.path)?.parent?.parent?.parent?.parent?.parent?.parent?.path;
427          if (engineSourcePath != null && (engineSourcePath == fs.path.dirname(engineSourcePath) || engineSourcePath.isEmpty)) {
428            engineSourcePath = null;
429            throwToolExit(userMessages.runnerNoEngineSrcDir(kFlutterEnginePackageName, kFlutterEngineEnvironmentVariableName),
430              exitCode: 2);
431          }
432        }
433      } on FileSystemException {
434        engineSourcePath = null;
435      } on FormatException {
436        engineSourcePath = null;
437      }
438      // If engineSourcePath is still not set, try to determine it by flutter root.
439      engineSourcePath ??= _tryEnginePath(fs.path.join(fs.directory(Cache.flutterRoot).parent.path, 'engine', 'src'));
440    }
441
442    if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
443      throwToolExit(userMessages.runnerNoEngineBuildDirInPath(engineSourcePath),
444        exitCode: 2);
445    }
446
447    return engineSourcePath;
448  }
449
450  String _getHostEngineBasename(String localEngineBasename) {
451    // Determine the host engine directory associated with the local engine:
452    // Strip '_sim_' since there are no host simulator builds.
453    String tmpBasename = localEngineBasename.replaceFirst('_sim_', '_');
454    tmpBasename = tmpBasename.substring(tmpBasename.indexOf('_') + 1);
455    // Strip suffix for various archs.
456    final List<String> suffixes = <String>['_arm', '_arm64', '_x86', '_x64'];
457    for (String suffix in suffixes) {
458      tmpBasename = tmpBasename.replaceFirst(RegExp('$suffix\$'), '');
459    }
460    return 'host_' + tmpBasename;
461  }
462
463  EngineBuildPaths _findEngineBuildPath(ArgResults globalResults, String enginePath) {
464    String localEngine;
465    if (globalResults['local-engine'] != null) {
466      localEngine = globalResults['local-engine'];
467    } else {
468      throwToolExit(userMessages.runnerLocalEngineRequired, exitCode: 2);
469    }
470
471    final String engineBuildPath = fs.path.normalize(fs.path.join(enginePath, 'out', localEngine));
472    if (!fs.isDirectorySync(engineBuildPath)) {
473      throwToolExit(userMessages.runnerNoEngineBuild(engineBuildPath), exitCode: 2);
474    }
475
476    final String basename = fs.path.basename(engineBuildPath);
477    final String hostBasename = _getHostEngineBasename(basename);
478    final String engineHostBuildPath = fs.path.normalize(fs.path.join(fs.path.dirname(engineBuildPath), hostBasename));
479    if (!fs.isDirectorySync(engineHostBuildPath)) {
480      throwToolExit(userMessages.runnerNoEngineBuild(engineHostBuildPath), exitCode: 2);
481    }
482
483    return EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath);
484  }
485
486  static void initFlutterRoot() {
487    Cache.flutterRoot ??= defaultFlutterRoot;
488  }
489
490  /// Get the root directories of the repo - the directories containing Dart packages.
491  List<String> getRepoRoots() {
492    final String root = fs.path.absolute(Cache.flutterRoot);
493    // not bin, and not the root
494    return <String>['dev', 'examples', 'packages'].map<String>((String item) {
495      return fs.path.join(root, item);
496    }).toList();
497  }
498
499  /// Get all pub packages in the Flutter repo.
500  List<Directory> getRepoPackages() {
501    return getRepoRoots()
502      .expand<String>((String root) => _gatherProjectPaths(root))
503      .map<Directory>((String dir) => fs.directory(dir))
504      .toList();
505  }
506
507  static List<String> _gatherProjectPaths(String rootPath) {
508    if (fs.isFileSync(fs.path.join(rootPath, '.dartignore')))
509      return <String>[];
510
511
512    final List<String> projectPaths = fs.directory(rootPath)
513      .listSync(followLinks: false)
514      .expand((FileSystemEntity entity) {
515        if (entity is Directory && !fs.path.split(entity.path).contains('.dart_tool')) {
516          return _gatherProjectPaths(entity.path);
517        }
518        return <String>[];
519      })
520      .toList();
521
522    if (fs.isFileSync(fs.path.join(rootPath, 'pubspec.yaml')))
523      projectPaths.add(rootPath);
524
525    return projectPaths;
526  }
527
528  void _checkFlutterCopy() {
529    // If the current directory is contained by a flutter repo, check that it's
530    // the same flutter that is currently running.
531    String directory = fs.path.normalize(fs.path.absolute(fs.currentDirectory.path));
532
533    // Check if the cwd is a flutter dir.
534    while (directory.isNotEmpty) {
535      if (_isDirectoryFlutterRepo(directory)) {
536        if (!_compareResolvedPaths(directory, Cache.flutterRoot)) {
537          printError(userMessages.runnerWrongFlutterInstance(Cache.flutterRoot, directory));
538        }
539
540        break;
541      }
542
543      final String parent = fs.path.dirname(directory);
544      if (parent == directory)
545        break;
546      directory = parent;
547    }
548
549    // Check that the flutter running is that same as the one referenced in the pubspec.
550    if (fs.isFileSync(kPackagesFileName)) {
551      final PackageMap packageMap = PackageMap(kPackagesFileName);
552      Uri flutterUri;
553      try {
554        flutterUri = packageMap.map['flutter'];
555      } on FormatException {
556        // We're not quite sure why this can happen, perhaps the user
557        // accidentally edited the .packages file. Re-running pub should
558        // fix the issue, and we definitely shouldn't crash here.
559        printTrace('Failed to parse .packages file to check flutter dependency.');
560        return;
561      }
562
563      if (flutterUri != null && (flutterUri.scheme == 'file' || flutterUri.scheme == '')) {
564        // .../flutter/packages/flutter/lib
565        final Uri rootUri = flutterUri.resolve('../../..');
566        final String flutterPath = fs.path.normalize(fs.file(rootUri).absolute.path);
567
568        if (!fs.isDirectorySync(flutterPath)) {
569          printError(userMessages.runnerRemovedFlutterRepo(Cache.flutterRoot, flutterPath));
570        } else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) {
571          printError(userMessages.runnerChangedFlutterRepo(Cache.flutterRoot, flutterPath));
572        }
573      }
574    }
575  }
576
577  // Check if `bin/flutter` and `bin/cache/engine.stamp` exist.
578  bool _isDirectoryFlutterRepo(String directory) {
579    return
580      fs.isFileSync(fs.path.join(directory, 'bin/flutter')) &&
581      fs.isFileSync(fs.path.join(directory, 'bin/cache/engine.stamp'));
582  }
583}
584
585bool _compareResolvedPaths(String path1, String path2) {
586  path1 = fs.directory(fs.path.absolute(path1)).resolveSymbolicLinksSync();
587  path2 = fs.directory(fs.path.absolute(path2)).resolveSymbolicLinksSync();
588
589  return path1 == path2;
590}
591