This change makes `flutter pub` show the help usage:
```
PS > flutter pub
Commands for managing Flutter packages.
Global options:
-h, --help Print this usage information.
-v, --verbose Noisy logging, including all shell commands executed.
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.)
-d, --device-id Target device id or name (prefixes allowed).
--version Reports the version of this tool.
--suppress-analytics Suppress analytics reporting for the current CLI invocation.
--disable-telemetry Disable telemetry reporting when this command runs.
Usage: flutter pub <subcommand> [arguments]
-h, --help Print this usage information.
Available subcommands:
add Add a dependency to pubspec.yaml.
cache Work with the Pub system cache.
deps Print package dependencies.
downgrade Downgrade packages in a Flutter project.
get Get the current package's dependencies.
global Work with Pub global packages.
login Log into pub.dev.
logout Log out of pub.dev.
outdated Analyze dependencies to find which ones can be upgraded.
pub Pass the remaining arguments to Dart's "pub" tool.
publish Publish the current package to pub.dartlang.org.
remove Removes a dependency from the current package.
run Run an executable from a package.
test Run the "test" package.
token Manage authentication tokens for hosted pub repositories.
upgrade Upgrade the current package's dependencies to latest versions.
uploader Manage uploaders for a package on pub.dev.
version Print Pub version.
Run "flutter help" to see global options.
```
Previously it showed an error message:
```
PS > flutter pub
Missing subcommand for "flutter pub".
Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.
```
Addresses https://github.com/flutter/flutter/issues/110025
350 lines
14 KiB
Dart
350 lines
14 KiB
Dart
// 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 for the current CLI invocation.');
|
|
argParser.addFlag('disable-telemetry',
|
|
negatable: false,
|
|
help: 'Disable telemetry reporting each time a flutter or dart '
|
|
'command runs, until it is re-enabled.');
|
|
argParser.addFlag('enable-telemetry',
|
|
negatable: false,
|
|
help: 'Enable telemetry reporting each time a flutter or dart '
|
|
'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" (see above).');
|
|
|
|
argParser.addOption('local-web-sdk',
|
|
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 web sdk if you have built multiple engine targets.\n'
|
|
'This path is relative to "--local-engine-src-path" (see above).');
|
|
|
|
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 invocations of 'build', 'custom-devices', and 'pub' print out
|
|
// their 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'];
|
|
} else if (args.first == 'pub') {
|
|
args = <String>['pub', '-h'];
|
|
}
|
|
}
|
|
|
|
return super.run(args);
|
|
}
|
|
|
|
@override
|
|
Future<void> runCommand(ArgResults topLevelResults) async {
|
|
final Map<Type, Object?> contextOverrides = <Type, Object?>{};
|
|
|
|
// If the flag for enabling or disabling telemetry is passed in,
|
|
// we will return out
|
|
if (topLevelResults.wasParsed('disable-telemetry') ||
|
|
topLevelResults.wasParsed('enable-telemetry')) {
|
|
return;
|
|
}
|
|
|
|
// 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?) ?? false)
|
|
|| topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
|
|
FlutterTesterDevices.showFlutterTesterDevice = true;
|
|
}
|
|
if (((topLevelResults['show-web-server-device'] as bool?) ?? false)
|
|
|| topLevelResults['device-id'] == WebServerDevice.kWebServerDeviceId) {
|
|
WebServerDevice.showWebServerDevice = true;
|
|
}
|
|
|
|
// Set up the tooling configuration.
|
|
final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath(
|
|
engineSourcePath: topLevelResults['local-engine-src-path'] as String?,
|
|
localEngine: topLevelResults['local-engine'] as String?,
|
|
localWebSdk: topLevelResults['local-web-sdk'] as String?,
|
|
packagePath: 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?) ?? false;
|
|
|
|
if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
|
|
await globals.cache.lock();
|
|
}
|
|
|
|
if ((topLevelResults['suppress-analytics'] as bool?) ?? false) {
|
|
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.
|
|
final String? specifiedDeviceId = topLevelResults['device-id'] as String?;
|
|
if (specifiedDeviceId != null) {
|
|
globals.deviceManager?.specifiedDeviceId = specifiedDeviceId;
|
|
}
|
|
|
|
if ((topLevelResults['version'] as bool?) ?? false) {
|
|
globals.flutterUsage.sendCommand('version');
|
|
globals.flutterVersion.fetchTagsAndUpdate();
|
|
String status;
|
|
if (machineFlag) {
|
|
final Map<String, Object> jsonOut = globals.flutterVersion.toJson();
|
|
jsonOut['flutterRoot'] = Cache.flutterRoot!;
|
|
status = const JsonEncoder.withIndent(' ').convert(jsonOut);
|
|
} else {
|
|
status = globals.flutterVersion.toString();
|
|
}
|
|
globals.printStatus(status);
|
|
return;
|
|
}
|
|
if (machineFlag && topLevelResults.command?.name != 'analyze') {
|
|
throwToolExit('The "--machine" flag is only valid with the "--version" flag or the "analyze --suggestions" command.', 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;
|
|
}
|
|
}
|