blob: 6a86fe48ee444f5dc0c15fccb385358e4d1d1272 [file]
// 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:flutter_tools_core/flutter_tools_core.dart';
import 'package:flutter_tools_extension/flutter_tools_extension.dart';
import '../android/android_sdk.dart';
import '../android/android_studio.dart';
import '../android/java.dart';
import '../base/common.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../context/android_context.dart';
import '../context/tool_context.dart';
import '../convert.dart';
import '../experimental/config.dart';
import '../experimental/extension_arg_parser.dart';
import '../experimental/extension_manager.dart';
import '../features.dart';
import '../ios/code_signing.dart';
import '../ios/plist_parser.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
class ConfigCommand extends FlutterCommand with ExtensionArgParserMixin {
ConfigCommand({
required AndroidContext androidContext,
required ToolContext toolContext,
required this.featureFlags,
bool verboseHelp = false,
ExtensionManager? extensionManager,
}) : _androidContext = androidContext,
_toolContext = toolContext,
_extensionManager = extensionManager,
_verboseHelp = verboseHelp,
super(toolContext: toolContext);
final AndroidContext _androidContext;
final ToolContext _toolContext;
final FeatureFlags featureFlags;
final ExtensionManager? _extensionManager;
final bool _verboseHelp;
@override
ToolContext get toolContext => _toolContext;
var _extensionSettingsGroups = const <ExtensionSettingsGroup>[];
@override
ArgParser createBaseArgParser() {
final ArgParser parser = super.createBaseArgParser();
parser.addFlag('list', help: 'List all settings and their current values.', negatable: false);
parser.addFlag(
'analytics',
hide: !_verboseHelp,
help:
'Enable or disable reporting anonymously tool usage statistics and crash reports.\n'
'(An alias for "--${FlutterGlobalOptions.kEnableAnalyticsFlag}" '
'and "--${FlutterGlobalOptions.kDisableAnalyticsFlag}" top level flags.)',
);
parser.addFlag(
'clear-ios-signing-settings',
negatable: false,
aliases: <String>['clear-ios-signing-cert'],
help:
'Clear the saved development certificate or provisioning profile choice used to sign apps for iOS device deployment.',
);
parser.addFlag(
'select-ios-signing-settings',
negatable: false,
help:
'Complete prompt to select and save code signing settings used to sign apps for iOS device deployment.',
);
parser.addOption('android-sdk', help: 'The Android SDK directory.');
parser.addOption(
'android-studio-dir',
help:
'The Android Studio installation directory. If unset, flutter will search for valid installations at well-known locations.',
);
parser.addOption(
'jdk-dir',
help:
'The Java Development Kit (JDK) installation directory. '
'If unset, flutter will search for one in the following order:\n'
' 1) the JDK bundled with the latest installation of Android Studio,\n'
' 2) the JDK found at the directory found in the JAVA_HOME environment variable, and\n'
" 3) the directory containing the java binary found in the user's path.",
);
parser.addOption(
'build-dir',
help: 'The relative path to override a projects build directory.',
valueHelp: 'out/',
);
parser.addFlag(
FlutterGlobalOptions.kMachineFlag,
negatable: false,
help: 'Outputs in a machine readable structured JSON format.',
hide: !_verboseHelp,
);
for (final Feature feature in featureFlags.allFeatures) {
final String? configSetting = feature.configSetting;
if (configSetting == null) {
continue;
}
final String channel = _toolContext.flutterVersion.channel;
parser.addFlag(
configSetting,
help: feature.generateHelpMessage(),
defaultsTo: feature.getSettingForChannel(channel).enabledByDefault,
);
}
parser.addFlag(
'clear-features',
help: 'Remove all configured features and restore them to the default values.',
negatable: false,
);
return parser;
}
Future<ExtensionConfiguration?> get _activeExtensionConfig async {
if (_extensionManager case final extensionManager?) {
await extensionManager.ensureInitialized();
final List<ConfigurationExtension> extensions = extensionManager.configurationExtensions;
if (extensions.isNotEmpty) {
return ExtensionConfiguration(extensions: extensions, logger: _toolContext.logger);
}
}
return null;
}
@override
Future<void> initializeDynamicOptions() async {
if (await _activeExtensionConfig case final activeConfig?) {
_extensionSettingsGroups = await activeConfig.fetchExtensionSettings();
if (_extensionSettingsGroups.isNotEmpty) {
rebuildDynamicArgParser();
}
}
}
@override
ArgParser buildDynamicArgParser(ArgParser dynamicParser) {
for (final ExtensionSettingsGroup(:featureFlags, :configOptions) in _extensionSettingsGroups) {
for (final FeatureFlag(:name, :help, :enabledByDefault) in featureFlags) {
if (!dynamicParser.options.containsKey(name)) {
dynamicParser.addFlag(name, help: help, defaultsTo: enabledByDefault);
} else {
_toolContext.logger.printTrace(
'Extension feature flag "$name" conflicts with an existing option and was skipped.',
);
}
}
for (final ConfigOption(:name, :help, :value) in configOptions) {
if (!dynamicParser.options.containsKey(name)) {
dynamicParser.addOption(name, help: help, defaultsTo: value);
} else {
_toolContext.logger.printTrace(
'Extension config option "$name" conflicts with an existing option and was skipped.',
);
}
}
}
return dynamicParser;
}
@override
final name = 'config';
@override
final description =
'Configure Flutter settings.\n\n'
'To remove a setting, configure it to an empty string.\n\n'
'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve '
"Flutter tools over time. See Google's privacy policy: https://www.google.com/intl/en/policies/privacy/";
@override
final category = FlutterCommandCategory.sdk;
@override
final aliases = <String>['configure'];
@override
bool get shouldUpdateCache => false;
@override
String get usageFooter => '\n$analyticsUsage';
/// Return null to disable analytics recording of the `config` command.
@override
Future<String?> get usagePath async => null;
@override
Future<FlutterCommandResult> runCommand() async {
final Logger logger = _toolContext.logger;
final Config config = _toolContext.config;
final FileSystem fs = _toolContext.fs;
final List<String> rest = argResults!.rest;
if (rest.isNotEmpty) {
throwToolExit(
exitCode: 2,
'error: flutter config: Too many arguments.\n'
'\n'
'If a value has a space in it, enclose in quotes on the command line\n'
'to make a single argument. For example:\n'
' flutter config --android-studio-dir "/opt/Android Studio"',
);
}
if (boolArg('list')) {
logger.printStatus(await settingsText);
return FlutterCommandResult.success();
}
if (outputMachineFormat) {
await handleMachine();
return FlutterCommandResult.success();
}
if (boolArg('clear-features')) {
for (final Feature feature in featureFlags.allFeatures) {
final String? configSetting = feature.configSetting;
if (configSetting != null) {
config.removeValue(configSetting);
}
}
final ExtensionConfiguration? activeConfig = await _activeExtensionConfig;
final List<ExtensionSettingsGroup> groups = _extensionSettingsGroups.isNotEmpty
? _extensionSettingsGroups
: await activeConfig?.fetchExtensionSettings() ?? const <ExtensionSettingsGroup>[];
for (final ExtensionSettingsGroup(:featureFlags) in groups) {
for (final FeatureFlag(:name) in featureFlags) {
config.removeValue(name);
}
}
logger.printStatus(requireReloadTipText);
return FlutterCommandResult.success();
}
if (argResults!.wasParsed('analytics')) {
final bool value = boolArg('analytics');
logger.printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
await analytics.setTelemetry(value);
}
if (argResults!.wasParsed('android-sdk')) {
_updateConfig('android-sdk', stringArg('android-sdk'));
}
if (argResults!.wasParsed('android-studio-dir')) {
_updateConfig('android-studio-dir', stringArg('android-studio-dir'));
}
if (argResults!.wasParsed('jdk-dir')) {
_updateConfig('jdk-dir', stringArg('jdk-dir'));
}
if (argResults!.wasParsed('clear-ios-signing-settings')) {
XcodeCodeSigningSettings.resetSettings(config, logger);
}
if (argResults!.wasParsed('select-ios-signing-settings')) {
final settings = XcodeCodeSigningSettings(
config: config,
logger: logger,
platform: _toolContext.platform,
processUtils: _toolContext.processUtils,
fileSystem: fs,
fileSystemUtils: _toolContext.fileSystemUtils,
terminal: _toolContext.terminal,
plistParser: PlistParser(
fileSystem: fs,
logger: logger,
processManager: _toolContext.processManager,
),
);
await settings.selectSettings();
}
if (argResults!.wasParsed('build-dir')) {
final String buildDir = stringArg('build-dir')!;
if (fs.path.isAbsolute(buildDir)) {
throwToolExit('build-dir should be a relative path');
}
_updateConfig('build-dir', buildDir);
}
for (final Feature feature in featureFlags.allFeatures) {
final String? configSetting = feature.configSetting;
if (configSetting == null) {
continue;
}
if (argResults!.wasParsed(configSetting)) {
final bool keyValue = boolArg(configSetting);
config.setValue(configSetting, keyValue);
logger.printStatus('Setting "$configSetting" value to "$keyValue".');
if (!keyValue && feature.warningMessageOnDisable != null) {
logger.printWarning(feature.warningMessageOnDisable!);
}
}
}
for (final ExtensionSettingsGroup(:featureFlags, :configOptions) in _extensionSettingsGroups) {
for (final FeatureFlag(:name) in featureFlags) {
if (argResults!.wasParsed(name)) {
final bool keyValue = boolArg(name);
config.setValue(name, keyValue);
logger.printStatus('Setting "$name" value to "$keyValue".');
}
}
for (final ConfigOption(:name) in configOptions) {
if (argResults!.wasParsed(name)) {
_updateConfig(name, stringArg(name));
}
}
}
if (argResults == null || argResults!.arguments.isEmpty) {
logger.printStatus(usage);
} else {
logger.printStatus('\n$requireReloadTipText');
}
return FlutterCommandResult.success();
}
Future<void> handleMachine() async {
final Config config = _toolContext.config;
final Logger logger = _toolContext.logger;
// Get all the current values.
final results = <String, Object?>{
for (final String key in config.keys) key: config.getValue(key),
};
// Ensure we send any calculated ones, if overrides don't exist.
final AndroidStudio? androidStudio = _androidContext.androidStudio;
if (results['android-studio-dir'] == null && androidStudio != null) {
results['android-studio-dir'] = androidStudio.directory;
}
final AndroidSdk? androidSdk = _androidContext.androidSdk;
if (results['android-sdk'] == null && androidSdk != null) {
results['android-sdk'] = androidSdk.directory.path;
}
final Java? java = _androidContext.java;
if (results['jdk-dir'] == null && java != null) {
results['jdk-dir'] = java.javaHome;
}
logger.printStatus(const JsonEncoder.withIndent(' ').convert(results));
}
void _updateConfig(String keyName, String? keyValue) {
final Config config = _toolContext.config;
final Logger logger = _toolContext.logger;
if (keyValue == null || keyValue.isEmpty) {
config.removeValue(keyName);
logger.printStatus('Removing "$keyName" value.');
} else {
config.setValue(keyName, keyValue);
logger.printStatus('Setting "$keyName" value to "$keyValue".');
}
}
/// List all config settings. for feature flags, include whether they are available.
Future<String> get settingsText async {
final Config config = _toolContext.config;
final featuresByName = <String, Feature>{
for (final feature in featureFlags.allFeatures)
if (feature.configSetting case final configSetting?) configSetting: feature,
};
final String channel = _toolContext.flutterVersion.channel;
final keys = <String>{
...featureFlags.allFeatures.map((Feature e) => e.configSetting).whereType<String>(),
...config.keys,
};
final Iterable<String> settings = keys.map<String>((String key) {
Object? value = config.getValue(key);
value ??= '(Not set)';
final buffer = StringBuffer(' $key: $value');
if (featuresByName[key] case final feature?) {
final FeatureChannelSetting setting = feature.getSettingForChannel(channel);
if (!setting.available) {
buffer.write(' (Unavailable)');
}
}
return buffer.toString();
});
final buffer = StringBuffer();
buffer.writeln('All Settings:');
if (settings.isEmpty) {
buffer.writeln(' No configs have been configured.');
} else {
buffer.writeln(settings.join('\n'));
}
final ExtensionConfiguration? activeConfig = await _activeExtensionConfig;
final List<ExtensionSettingsGroup> groups = _extensionSettingsGroups.isNotEmpty
? _extensionSettingsGroups
: (await activeConfig?.fetchExtensionSettings()) ?? const <ExtensionSettingsGroup>[];
if (groups.any((ExtensionSettingsGroup g) => g.isNotEmpty)) {
buffer.writeln('\nExtension Settings:');
for (final ExtensionSettingsGroup(:title, :featureFlags, :configOptions, :isEmpty)
in groups) {
if (isEmpty) {
continue;
}
buffer.writeln(' $title:');
for (final FeatureFlag(:name, :enabledByDefault) in featureFlags) {
final Object val = config.getValue(name) ?? enabledByDefault;
buffer.writeln(' $name: $val');
}
for (final ConfigOption(:name, :value) in configOptions) {
final Object val = config.getValue(name) ?? value ?? '(Not set)';
buffer.writeln(' $name: $val');
}
}
}
return buffer.toString();
}
/// List the status of the analytics reporting.
String get analyticsUsage {
return 'Analytics reporting is currently ${analytics.telemetryEnabled ? 'enabled' : 'disabled'}.';
}
/// Raising the reload tip for setting changes.
final requireReloadTipText =
'You may need to restart any open editors for them to read new settings.';
}