blob: 396756808e863a15efb9e5a3530a0c5923fa4f78 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:completion/completion.dart';
import 'package:file/file.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/terminal.dart';
import '../base/user_messages.dart';
import '../base/utils.dart';
import '../cache.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import '../tester/flutter_tester.dart';
import '../web/web_device.dart';
class FlutterCommandRunner extends CommandRunner<void> {
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.\n'
'If used with "--help", shows hidden options. '
'If used with "flutter doctor", shows additional diagnostic information. '
'(Use "-vv" to force verbose logging in those cases.)');
argParser.addFlag('prefixed-errors',
negatable: false,
help: 'Causes lines sent to stderr to be prefixed with "ERROR:".',
hide: !verboseHelp);
argParser.addFlag('quiet',
negatable: false,
hide: !verboseHelp,
help: 'Reduce the amount of output from some commands.');
argParser.addFlag('wrap',
hide: !verboseHelp,
help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
defaultsTo: true);
argParser.addOption('wrap-column',
hide: !verboseHelp,
help: 'Sets the output wrap column. If not set, uses the width of the terminal. No '
'wrapping occurs if not writing to a terminal. Use "--no-wrap" to turn off wrapping '
'when connected to a terminal.');
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: !verboseHelp,
help: 'When used with the "--version" flag, outputs the information using JSON.');
argParser.addFlag('color',
hide: !verboseHelp,
help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
defaultsTo: true);
argParser.addFlag('version-check',
defaultsTo: true,
hide: !verboseHelp,
help: 'Allow Flutter to check for updates when this command runs.');
argParser.addFlag('suppress-analytics',
negatable: false,
help: 'Suppress analytics reporting when this command runs.');
argParser.addOption('packages',
hide: !verboseHelp,
help: 'Path to your "package_config.json" file.');
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 dependency_overrides for $kFlutterEnginePackageName, '
'if any.');
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" or "--local-engine-src-out" (q.v.).');
if (verboseHelp) {
argParser.addSeparator('Options for testing the "flutter" tool itself:');
}
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 test Flutter tooling.');
argParser.addFlag('show-web-server-device',
negatable: false,
hide: !verboseHelp,
help: 'List the special "web-server" device in device listings.',
);
}
@override
ArgParser get argParser => _argParser;
final ArgParser _argParser = ArgParser(
allowTrailingOptions: false,
usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
);
@override
String get usageFooter {
return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.',
columnWidth: globals.outputPreferences.wrapColumn,
shouldWrap: globals.outputPreferences.wrapText,
);
}
@override
String get usage {
final String usageWithoutDescription = super.usage.substring(description.length + 2);
final String prefix = wrapText(description,
shouldWrap: globals.outputPreferences.wrapText,
columnWidth: globals.outputPreferences.wrapColumn,
);
return '$prefix\n\n$usageWithoutDescription';
}
@override
ArgResults parse(Iterable<String> args) {
try {
// This is where the CommandRunner would call argParser.parse(args). We
// override this function so we can call tryArgsCompletion instead, so the
// completion package can interrogate the argParser, and as part of that,
// it calls argParser.parse(args) itself and returns the result.
return tryArgsCompletion(args.toList(), argParser);
} on ArgParserException catch (error) {
if (error.commands.isEmpty) {
usageException(error.message);
}
Command<void>? command = commands[error.commands.first];
for (final String commandName in error.commands.skip(1)) {
command = command?.subcommands[commandName];
}
command!.usageException(error.message);
}
}
@override
Future<void> 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) {
if (args.first == 'build') {
args = <String>['build', '-h'];
} else if (args.first == 'custom-devices') {
args = <String>['custom-devices', '-h'];
}
}
return super.run(args);
}
@override
Future<void> runCommand(ArgResults topLevelResults) async {
final Map<Type, Object?> contextOverrides = <Type, Object?>{};
// Don't set wrapColumns unless the user said to: if it's set, then all
// wrapping will occur at this width explicitly, and won't adapt if the
// terminal size changes during a run.
int? wrapColumn;
if (topLevelResults.wasParsed('wrap-column')) {
try {
wrapColumn = int.parse(topLevelResults['wrap-column'] as String);
if (wrapColumn < 0) {
throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column']));
}
} on FormatException {
throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column']));
}
}
// If we're not writing to a terminal with a defined width, then don't wrap
// anything, unless the user explicitly said to.
final bool useWrapping = topLevelResults.wasParsed('wrap')
? topLevelResults['wrap'] as bool
: globals.stdio.terminalColumns != null && topLevelResults['wrap'] as bool;
contextOverrides[OutputPreferences] = OutputPreferences(
wrapText: useWrapping,
showColor: topLevelResults['color'] as bool?,
wrapColumn: wrapColumn,
);
if ((topLevelResults['show-test-device'] as bool?) == true ||
topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
FlutterTesterDevices.showFlutterTesterDevice = true;
}
if ((topLevelResults['show-web-server-device'] as bool?) == true ||
topLevelResults['device-id'] == WebServerDevice.kWebServerDeviceId) {
WebServerDevice.showWebServerDevice = true;
}
// Set up the tooling configuration.
final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath(
topLevelResults['local-engine-src-path'] as String?,
topLevelResults['local-engine'] as String?,
topLevelResults['packages'] as String?,
);
if (engineBuildPaths != null) {
contextOverrides.addAll(<Type, Object?>{
Artifacts: Artifacts.getLocalEngine(engineBuildPaths),
});
}
await context.run<void>(
overrides: contextOverrides.map<Type, Generator>((Type type, Object? value) {
return MapEntry<Type, Generator>(type, () => value);
}),
body: () async {
globals.logger.quiet = (topLevelResults['quiet'] as bool?) == true;
if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
await globals.cache.lock();
}
if ((topLevelResults['suppress-analytics'] as bool?) == true) {
globals.flutterUsage.suppressAnalytics = true;
}
globals.flutterVersion.ensureVersionFile();
final bool machineFlag = topLevelResults['machine'] as bool? ?? false;
final bool ci = await globals.botDetector.isRunningOnBot;
final bool redirectedCompletion = !globals.stdio.hasTerminal &&
(topLevelResults.command?.name ?? '').endsWith('-completion');
final bool isMachine = machineFlag || ci || redirectedCompletion;
final bool versionCheckFlag = topLevelResults['version-check'] as bool? ?? false;
final bool explicitVersionCheckPassed = topLevelResults.wasParsed('version-check') && versionCheckFlag;
if (topLevelResults.command?.name != 'upgrade' &&
(explicitVersionCheckPassed || (versionCheckFlag && !isMachine))) {
await globals.flutterVersion.checkFlutterVersionFreshness();
}
// See if the user specified a specific device.
globals.deviceManager?.specifiedDeviceId = topLevelResults['device-id'] as String?;
if ((topLevelResults['version'] as bool?) == true) {
globals.flutterUsage.sendCommand('version');
globals.flutterVersion.fetchTagsAndUpdate();
String status;
if (machineFlag) {
final Map<String, Object> jsonOut = globals.flutterVersion.toJson();
if (jsonOut != null) {
jsonOut['flutterRoot'] = Cache.flutterRoot!;
}
status = const JsonEncoder.withIndent(' ').convert(jsonOut);
} else {
status = globals.flutterVersion.toString();
}
globals.printStatus(status);
return;
}
if (machineFlag) {
throwToolExit('The "--machine" flag is only valid with the "--version" flag.', exitCode: 2);
}
await super.runCommand(topLevelResults);
},
);
}
/// Get the root directories of the repo - the directories containing Dart packages.
List<String> getRepoRoots() {
final String root = globals.fs.path.absolute(Cache.flutterRoot!);
// not bin, and not the root
return <String>['dev', 'examples', 'packages'].map<String>((String item) {
return globals.fs.path.join(root, item);
}).toList();
}
/// Get all pub packages in the Flutter repo.
List<Directory> getRepoPackages() {
return getRepoRoots()
.expand<String>((String root) => _gatherProjectPaths(root))
.map<Directory>((String dir) => globals.fs.directory(dir))
.toList();
}
static List<String> _gatherProjectPaths(String rootPath) {
if (globals.fs.isFileSync(globals.fs.path.join(rootPath, '.dartignore'))) {
return <String>[];
}
final List<String> projectPaths = globals.fs.directory(rootPath)
.listSync(followLinks: false)
.expand((FileSystemEntity entity) {
if (entity is Directory && !globals.fs.path.split(entity.path).contains('.dart_tool')) {
return _gatherProjectPaths(entity.path);
}
return <String>[];
})
.toList();
if (globals.fs.isFileSync(globals.fs.path.join(rootPath, 'pubspec.yaml'))) {
projectPaths.add(rootPath);
}
return projectPaths;
}
}