| // Copyright 2015 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 'package:args/args.dart'; |
| import 'package:args/command_runner.dart'; |
| import 'package:file/file.dart'; |
| import 'package:platform/platform.dart'; |
| import 'package:process/process.dart'; |
| |
| import '../artifacts.dart'; |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/flags.dart'; |
| import '../base/logger.dart'; |
| import '../base/os.dart'; |
| import '../base/platform.dart'; |
| import '../base/process.dart'; |
| import '../base/process_manager.dart'; |
| import '../base/utils.dart'; |
| import '../cache.dart'; |
| import '../dart/package_map.dart'; |
| import '../device.dart'; |
| import '../globals.dart'; |
| import '../tester/flutter_tester.dart'; |
| import '../usage.dart'; |
| import '../version.dart'; |
| import '../vmservice.dart'; |
| |
| const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo) |
| const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo) |
| const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/ |
| const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/ |
| const String kFlutterEnginePackageName = 'sky_engine'; |
| |
| class FlutterCommandRunner extends CommandRunner<Null> { |
| FlutterCommandRunner({ bool verboseHelp: false }) : super( |
| 'flutter', |
| 'Manage your Flutter app development.\n' |
| '\n' |
| 'Common commands:\n' |
| '\n' |
| ' flutter create <output directory>\n' |
| ' Create a new Flutter project in the specified directory.\n' |
| '\n' |
| ' flutter run [options]\n' |
| ' Run your Flutter application on an attached device or in an emulator.', |
| ) { |
| argParser.addFlag('verbose', |
| abbr: 'v', |
| negatable: false, |
| help: 'Noisy logging, including all shell commands executed.'); |
| argParser.addFlag('quiet', |
| negatable: false, |
| hide: !verboseHelp, |
| help: 'Reduce the amount of output from some commands.'); |
| argParser.addOption('device-id', |
| abbr: 'd', |
| help: 'Target device id or name (prefixes allowed).'); |
| argParser.addFlag('version', |
| negatable: false, |
| help: 'Reports the version of this tool.'); |
| argParser.addFlag('machine', |
| negatable: false, |
| hide: true); |
| argParser.addFlag('color', |
| negatable: true, |
| hide: !verboseHelp, |
| help: 'Whether to use terminal colors.'); |
| argParser.addFlag('suppress-analytics', |
| negatable: false, |
| hide: !verboseHelp, |
| help: 'Suppress analytics reporting when this command runs.'); |
| argParser.addFlag('bug-report', |
| negatable: false, |
| help: |
| 'Captures a bug report file to submit to the Flutter team ' |
| '(contains local paths, device\nidentifiers, and log snippets).'); |
| argParser.addFlag('show-test-device', |
| negatable: false, |
| hide: !verboseHelp, |
| help: 'List the special \'flutter-tester\' device in device listings. ' |
| 'This headless device is used to\ntest Flutter tooling.'); |
| |
| String packagesHelp; |
| if (fs.isFileSync(kPackagesFileName)) |
| packagesHelp = '\n(defaults to "$kPackagesFileName")'; |
| else |
| packagesHelp = '\n(required, since the current directory does not contain a "$kPackagesFileName" file)'; |
| argParser.addOption('packages', |
| hide: !verboseHelp, |
| help: 'Path to your ".packages" file.$packagesHelp'); |
| argParser.addOption('flutter-root', |
| help: 'The root directory of the Flutter repository (uses \$$kFlutterRootEnvironmentVariableName if set).'); |
| |
| if (verboseHelp) |
| argParser.addSeparator('Local build selection options (not normally required):'); |
| |
| argParser.addOption('local-engine-src-path', |
| hide: !verboseHelp, |
| help: |
| 'Path to your engine src directory, if you are building Flutter locally.\n' |
| 'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to the path given in your pubspec.yaml\n' |
| 'dependency_overrides for $kFlutterEnginePackageName, if any, or, failing that, tries to guess at the location\n' |
| 'based on the value of the --flutter-root option.'); |
| |
| argParser.addOption('local-engine', |
| hide: !verboseHelp, |
| help: |
| 'Name of a build output within the engine out directory, if you are building Flutter locally.\n' |
| 'Use this to select a specific version of the engine if you have built multiple engine targets.\n' |
| 'This path is relative to --local-engine-src-path/out.'); |
| argParser.addOption('record-to', |
| hide: true, |
| help: |
| 'Enables recording of process invocations (including stdout and stderr of all such invocations),\n' |
| 'and file system access (reads and writes).\n' |
| 'Serializes that recording to a directory with the path specified in this flag. If the\n' |
| 'directory does not already exist, it will be created.'); |
| argParser.addOption('replay-from', |
| hide: true, |
| help: |
| 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n' |
| 'the specified recording (obtained via --record-to). The path specified in this flag must refer\n' |
| 'to a directory that holds serialized process invocations structured according to the output of\n' |
| '--record-to.'); |
| } |
| |
| @override |
| ArgParser get argParser => _argParser; |
| final ArgParser _argParser = new ArgParser(allowTrailingOptions: false); |
| |
| @override |
| String get usageFooter { |
| return 'Run "flutter help -v" for verbose help output, including less commonly used options.'; |
| } |
| |
| static String get _defaultFlutterRoot { |
| if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) |
| return platform.environment[kFlutterRootEnvironmentVariableName]; |
| try { |
| if (platform.script.scheme == 'data') |
| return '../..'; // we're running as a test |
| final String script = platform.script.toFilePath(); |
| if (fs.path.basename(script) == kSnapshotFileName) |
| return fs.path.dirname(fs.path.dirname(fs.path.dirname(script))); |
| if (fs.path.basename(script) == kFlutterToolsScriptFileName) |
| return fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(script)))); |
| |
| // If run from a bare script within the repo. |
| if (script.contains('flutter/packages/')) |
| return script.substring(0, script.indexOf('flutter/packages/') + 8); |
| if (script.contains('flutter/examples/')) |
| return script.substring(0, script.indexOf('flutter/examples/') + 8); |
| } catch (error) { |
| // we don't have a logger at the time this is run |
| // (which is why we don't use printTrace here) |
| print('Unable to locate flutter root: $error'); |
| } |
| return '.'; |
| } |
| |
| @override |
| Future<Null> run(Iterable<String> args) { |
| // Have an invocation of 'build' print out it's sub-commands. |
| // TODO(ianh): Move this to the Build command itself somehow. |
| if (args.length == 1 && args.first == 'build') |
| args = <String>['build', '-h']; |
| |
| return super.run(args); |
| } |
| |
| @override |
| Future<Null> runCommand(ArgResults topLevelResults) async { |
| final Map<Type, dynamic> contextOverrides = <Type, dynamic>{ |
| Flags: new Flags(topLevelResults), |
| }; |
| |
| // Check for verbose. |
| if (topLevelResults['verbose']) { |
| // Override the logger. |
| contextOverrides[Logger] = new VerboseLogger(logger); |
| } |
| |
| if (topLevelResults['show-test-device'] || |
| topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) { |
| FlutterTesterDevices.showFlutterTesterDevice = true; |
| } |
| |
| String recordTo = topLevelResults['record-to']; |
| String replayFrom = topLevelResults['replay-from']; |
| |
| if (topLevelResults['bug-report']) { |
| // --bug-report implies --record-to=<tmp_path> |
| final Directory tmp = await const LocalFileSystem() |
| .systemTempDirectory |
| .createTemp('flutter_tools_'); |
| recordTo = tmp.path; |
| |
| // Record the arguments that were used to invoke this runner. |
| final File manifest = tmp.childFile('MANIFEST.txt'); |
| final StringBuffer buffer = new StringBuffer() |
| ..writeln('# arguments') |
| ..writeln(topLevelResults.arguments) |
| ..writeln() |
| ..writeln('# rest') |
| ..writeln(topLevelResults.rest); |
| await manifest.writeAsString(buffer.toString(), flush: true); |
| |
| // ZIP the recording up once the recording has been serialized. |
| addShutdownHook(() async { |
| final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip'); |
| os.zip(tmp, zipFile); |
| printStatus( |
| 'Bug report written to ${zipFile.basename}.\n' |
| 'Note that this bug report contains local paths, device ' |
| 'identifiers, and log snippets.'); |
| }, ShutdownStage.POST_PROCESS_RECORDING); |
| addShutdownHook(() => tmp.delete(recursive: true), ShutdownStage.CLEANUP); |
| } |
| |
| assert(recordTo == null || replayFrom == null); |
| |
| if (recordTo != null) { |
| recordTo = recordTo.trim(); |
| if (recordTo.isEmpty) |
| throwToolExit('record-to location not specified'); |
| contextOverrides.addAll(<Type, dynamic>{ |
| ProcessManager: getRecordingProcessManager(recordTo), |
| FileSystem: getRecordingFileSystem(recordTo), |
| Platform: await getRecordingPlatform(recordTo), |
| }); |
| VMService.enableRecordingConnection(recordTo); |
| } |
| |
| if (replayFrom != null) { |
| replayFrom = replayFrom.trim(); |
| if (replayFrom.isEmpty) |
| throwToolExit('replay-from location not specified'); |
| contextOverrides.addAll(<Type, dynamic>{ |
| ProcessManager: await getReplayProcessManager(replayFrom), |
| FileSystem: getReplayFileSystem(replayFrom), |
| Platform: await getReplayPlatform(replayFrom), |
| }); |
| VMService.enableReplayConnection(replayFrom); |
| } |
| |
| // We must set Cache.flutterRoot early because other features use it (e.g. |
| // enginePath's initializer uses it). |
| final String flutterRoot = topLevelResults['flutter-root'] ?? _defaultFlutterRoot; |
| Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot)); |
| |
| // Set up the tooling configuration. |
| final String enginePath = _findEnginePath(topLevelResults); |
| if (enginePath != null) { |
| contextOverrides.addAll(<Type, dynamic>{ |
| Artifacts: Artifacts.getLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath)), |
| }); |
| } |
| |
| await context.run<Null>( |
| overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) { |
| return new MapEntry<Type, Generator>(type, () => value); |
| }), |
| body: () async { |
| logger.quiet = topLevelResults['quiet']; |
| |
| if (topLevelResults.wasParsed('color')) |
| logger.supportsColor = topLevelResults['color']; |
| |
| if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') |
| await Cache.lock(); |
| |
| if (topLevelResults['suppress-analytics']) |
| flutterUsage.suppressAnalytics = true; |
| |
| _checkFlutterCopy(); |
| await FlutterVersion.instance.ensureVersionFile(); |
| if (topLevelResults.command?.name != 'upgrade') { |
| await FlutterVersion.instance.checkFlutterVersionFreshness(); |
| } |
| |
| if (topLevelResults.wasParsed('packages')) |
| PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages'])); |
| |
| // See if the user specified a specific device. |
| deviceManager.specifiedDeviceId = topLevelResults['device-id']; |
| |
| if (topLevelResults['version']) { |
| flutterUsage.sendCommand('version'); |
| String status; |
| if (topLevelResults['machine']) { |
| status = const JsonEncoder.withIndent(' ').convert(FlutterVersion.instance.toJson()); |
| } else { |
| status = FlutterVersion.instance.toString(); |
| } |
| printStatus(status); |
| return; |
| } |
| |
| if (topLevelResults['machine']) { |
| throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2); |
| } |
| |
| await super.runCommand(topLevelResults); |
| }, |
| ); |
| } |
| |
| String _tryEnginePath(String enginePath) { |
| if (fs.isDirectorySync(fs.path.join(enginePath, 'out'))) |
| return enginePath; |
| return null; |
| } |
| |
| String _findEnginePath(ArgResults globalResults) { |
| String engineSourcePath = globalResults['local-engine-src-path'] ?? platform.environment[kFlutterEngineEnvironmentVariableName]; |
| |
| if (engineSourcePath == null && globalResults['local-engine'] != null) { |
| try { |
| final Uri engineUri = new PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName]; |
| if (engineUri != null) { |
| engineSourcePath = fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(engineUri.path)))); |
| final bool dirExists = fs.isDirectorySync(fs.path.join(engineSourcePath, 'out')); |
| if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists) |
| engineSourcePath = null; |
| } |
| } on FileSystemException { |
| engineSourcePath = null; |
| } on FormatException { |
| engineSourcePath = null; |
| } |
| |
| engineSourcePath ??= _tryEnginePath(fs.path.join(Cache.flutterRoot, '../engine/src')); |
| |
| if (engineSourcePath == null) { |
| throwToolExit('Unable to detect local Flutter engine build directory.\n' |
| 'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n' |
| 'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n' |
| 'use --local-engine-src-path to specify the path to the root of your flutter/engine repository.', |
| exitCode: 2); |
| } |
| } |
| |
| if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) { |
| throwToolExit('Unable to detect a Flutter engine build directory in $engineSourcePath.\n' |
| 'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that\n' |
| 'you have compiled the engine in that directory, which should produce an \'out\' directory', |
| exitCode: 2); |
| } |
| |
| return engineSourcePath; |
| } |
| |
| EngineBuildPaths _findEngineBuildPath(ArgResults globalResults, String enginePath) { |
| String localEngine; |
| if (globalResults['local-engine'] != null) { |
| localEngine = globalResults['local-engine']; |
| } else { |
| throwToolExit('You must specify --local-engine if you are using a locally built engine.', exitCode: 2); |
| } |
| |
| final String engineBuildPath = fs.path.normalize(fs.path.join(enginePath, 'out', localEngine)); |
| if (!fs.isDirectorySync(engineBuildPath)) { |
| throwToolExit('No Flutter engine build found at $engineBuildPath.', exitCode: 2); |
| } |
| |
| // Determine the host engine directory associated with the local engine: |
| // * strip '_sim_' since there are no host simulator builds. |
| // * replace the target platform with host. |
| final String basename = fs.path.basename(engineBuildPath); |
| final String hostBasename = 'host_' + basename.replaceFirst('_sim_', '_').substring(basename.indexOf('_') + 1); |
| final String engineHostBuildPath = fs.path.normalize(fs.path.join(fs.path.dirname(engineBuildPath), hostBasename)); |
| |
| return new EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath); |
| } |
| |
| static void initFlutterRoot() { |
| Cache.flutterRoot ??= _defaultFlutterRoot; |
| } |
| |
| /// Get all pub packages in the Flutter repo. |
| List<Directory> getRepoPackages() { |
| final String root = fs.path.absolute(Cache.flutterRoot); |
| // not bin, and not the root |
| return <String>['dev', 'examples', 'packages'] |
| .expand<String>((String path) => _gatherProjectPaths(fs.path.join(root, path))) |
| .map((String dir) => fs.directory(dir)) |
| .toList(); |
| } |
| |
| static List<String> _gatherProjectPaths(String rootPath) { |
| if (fs.isFileSync(fs.path.join(rootPath, '.dartignore'))) |
| return <String>[]; |
| |
| |
| final List<String> projectPaths = fs.directory(rootPath) |
| .listSync(followLinks: false) |
| .expand((FileSystemEntity entity) { |
| return entity is Directory ? _gatherProjectPaths(entity.path) : <String>[]; |
| }) |
| .toList(); |
| |
| if (fs.isFileSync(fs.path.join(rootPath, 'pubspec.yaml'))) |
| projectPaths.add(rootPath); |
| |
| return projectPaths; |
| } |
| |
| void _checkFlutterCopy() { |
| // If the current directory is contained by a flutter repo, check that it's |
| // the same flutter that is currently running. |
| String directory = fs.path.normalize(fs.path.absolute(fs.currentDirectory.path)); |
| |
| // Check if the cwd is a flutter dir. |
| while (directory.isNotEmpty) { |
| if (_isDirectoryFlutterRepo(directory)) { |
| if (!_compareResolvedPaths(directory, Cache.flutterRoot)) { |
| printError( |
| 'Warning: the \'flutter\' tool you are currently running is not the one from the current directory:\n' |
| ' running Flutter : ${Cache.flutterRoot}\n' |
| ' current directory: $directory\n' |
| 'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n' |
| 'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path).\n' |
| ); |
| } |
| |
| break; |
| } |
| |
| final String parent = fs.path.dirname(directory); |
| if (parent == directory) |
| break; |
| directory = parent; |
| } |
| |
| // Check that the flutter running is that same as the one referenced in the pubspec. |
| if (fs.isFileSync(kPackagesFileName)) { |
| final PackageMap packageMap = new PackageMap(kPackagesFileName); |
| final Uri flutterUri = packageMap.map['flutter']; |
| |
| if (flutterUri != null && (flutterUri.scheme == 'file' || flutterUri.scheme == '')) { |
| // .../flutter/packages/flutter/lib |
| final Uri rootUri = flutterUri.resolve('../../..'); |
| final String flutterPath = fs.path.normalize(fs.file(rootUri).absolute.path); |
| |
| if (!fs.isDirectorySync(flutterPath)) { |
| printError( |
| 'Warning! This package referenced a Flutter repository via the .packages file that is\n' |
| 'no longer available. The repository from which the \'flutter\' tool is currently\n' |
| 'executing will be used instead.\n' |
| ' running Flutter tool: ${Cache.flutterRoot}\n' |
| ' previous reference : $flutterPath\n' |
| 'This can happen if you deleted or moved your copy of the Flutter repository, or\n' |
| 'if it was on a volume that is no longer mounted or has been mounted at a\n' |
| 'different location. Please check your system path to verify that you are running\n' |
| 'the expected version (run \'flutter --version\' to see which flutter is on your path).\n' |
| ); |
| } else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) { |
| printError( |
| 'Warning! The \'flutter\' tool you are currently running is from a different Flutter\n' |
| 'repository than the one last used by this package. The repository from which the\n' |
| '\'flutter\' tool is currently executing will be used instead.\n' |
| ' running Flutter tool: ${Cache.flutterRoot}\n' |
| ' previous reference : $flutterPath\n' |
| 'This can happen when you have multiple copies of flutter installed. Please check\n' |
| 'your system path to verify that you are running the expected version (run\n' |
| '\'flutter --version\' to see which flutter is on your path).\n' |
| ); |
| } |
| } |
| } |
| } |
| |
| // Check if `bin/flutter` and `bin/cache/engine.stamp` exist. |
| bool _isDirectoryFlutterRepo(String directory) { |
| return |
| fs.isFileSync(fs.path.join(directory, 'bin/flutter')) && |
| fs.isFileSync(fs.path.join(directory, 'bin/cache/engine.stamp')); |
| } |
| } |
| |
| bool _compareResolvedPaths(String path1, String path2) { |
| path1 = fs.directory(fs.path.absolute(path1)).resolveSymbolicLinksSync(); |
| path2 = fs.directory(fs.path.absolute(path2)).resolveSymbolicLinksSync(); |
| |
| return path1 == path2; |
| } |