| // 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:meta/meta.dart'; | |
| import 'package:package_config/package_config_types.dart'; | |
| import 'package:unified_analytics/unified_analytics.dart'; | |
| import '../application_package.dart'; | |
| import '../base/common.dart'; | |
| import '../base/context.dart'; | |
| import '../base/file_system.dart'; | |
| import '../base/io.dart' as io; | |
| import '../base/io.dart'; | |
| import '../base/logger.dart'; | |
| import '../base/os.dart'; | |
| import '../base/platform.dart'; | |
| import '../base/signals.dart'; | |
| import '../base/terminal.dart'; | |
| import '../base/time.dart'; | |
| import '../base/user_messages.dart'; | |
| import '../base/utils.dart'; | |
| import '../build_info.dart'; | |
| import '../bundle.dart' as bundle; | |
| import '../cache.dart'; | |
| import '../context/tool_context.dart'; | |
| import '../convert.dart'; | |
| import '../dart/package_map.dart'; | |
| import '../dart/pub.dart'; | |
| import '../device.dart'; | |
| import '../features.dart'; | |
| import '../globals.dart' as globals; | |
| import '../persistent_tool_state.dart'; | |
| import '../pre_run_validator.dart'; | |
| import '../project.dart'; | |
| import '../reporting/unified_analytics.dart'; | |
| import '../version.dart'; | |
| import '../web/web_options.dart'; | |
| import 'flutter_command_runner.dart'; | |
| import 'options/common_options.dart'; | |
| import 'options/option_bundle.dart'; | |
| import 'options/option_descriptor.dart'; | |
| import 'options/safe_arg_results.dart'; | |
| import 'target_devices.dart'; | |
| export '../cache.dart' show DevelopmentArtifact; | |
| export 'options/common_options.dart'; | |
| export 'options/option_bundle.dart'; | |
| export 'options/option_descriptor.dart'; | |
| export 'options/safe_arg_results.dart'; | |
| abstract class DotEnvRegex { | |
| // Dot env multi-line block value regex | |
| static final multiLineBlock = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*"""\s*(.*)$'); | |
| // Dot env full line value regex (eg FOO=bar) | |
| // Entire line will be matched including key and value | |
| static final keyValue = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)?$'); | |
| // Dot env value wrapped in double quotes regex (eg FOO="bar") | |
| // Value between double quotes will be matched (eg only bar in "bar") | |
| static final doubleQuotedValue = RegExp(r'^"(.*)"\s*(\#\s*.*)?$'); | |
| // Dot env value wrapped in single quotes regex (eg FOO='bar') | |
| // Value between single quotes will be matched (eg only bar in 'bar') | |
| static final singleQuotedValue = RegExp(r"^'(.*)'\s*(\#\s*.*)?$"); | |
| // Dot env value wrapped in back quotes regex (eg FOO=`bar`) | |
| // Value between back quotes will be matched (eg only bar in `bar`) | |
| static final backQuotedValue = RegExp(r'^`(.*)`\s*(\#\s*.*)?$'); | |
| // Dot env value without quotes regex (eg FOO=bar) | |
| // Value without quotes will be matched (eg full value after the equals sign) | |
| static final unquotedValue = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$'); | |
| } | |
| abstract class _HttpRegex { | |
| // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 | |
| static const _vchar = r'\x21-\x7E'; | |
| static const _spaceOrTab = r'\x20\x09'; | |
| static const _nonDelimiterVchar = | |
| r'\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E'; | |
| // --web-header is provided as key=value for consistency with --dart-define | |
| static final httpHeader = RegExp( | |
| '^([$_nonDelimiterVchar]+)' | |
| r'\s*=\s*' | |
| '([$_vchar$_spaceOrTab]+)' | |
| r'$', | |
| ); | |
| } | |
| enum ExitStatus { success, warning, fail, killed } | |
| /// [FlutterCommand]s' subclasses' [FlutterCommand.runCommand] can optionally | |
| /// provide a [FlutterCommandResult] to furnish additional information for | |
| /// analytics. | |
| class FlutterCommandResult { | |
| const FlutterCommandResult(this.exitStatus, {this.timingLabelParts, this.endTimeOverride}); | |
| /// A command that succeeded. It is used to log the result of a command invocation. | |
| factory FlutterCommandResult.success() { | |
| return const FlutterCommandResult(ExitStatus.success); | |
| } | |
| /// A command that exited with a warning. It is used to log the result of a command invocation. | |
| factory FlutterCommandResult.warning() { | |
| return const FlutterCommandResult(ExitStatus.warning); | |
| } | |
| /// A command that failed. It is used to log the result of a command invocation. | |
| factory FlutterCommandResult.fail() { | |
| return const FlutterCommandResult(ExitStatus.fail); | |
| } | |
| final ExitStatus exitStatus; | |
| /// Optional data that can be appended to the timing event. | |
| /// https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#timingLabel | |
| /// Do not add PII. | |
| final List<String?>? timingLabelParts; | |
| /// Optional epoch time when the command's non-interactive wait time is | |
| /// complete during the command's execution. Use to measure user perceivable | |
| /// latency without measuring user interaction time. | |
| /// | |
| /// [FlutterCommand] will automatically measure and report the command's | |
| /// complete time if not overridden. | |
| final DateTime? endTimeOverride; | |
| @override | |
| String toString() => exitStatus.name; | |
| } | |
| /// Common flutter command line options. | |
| abstract final class FlutterOptions { | |
| static const kFrontendServerStarterPath = 'frontend-server-starter-path'; | |
| static const kExtraFrontEndOptions = 'extra-front-end-options'; | |
| static const kExtraGenSnapshotOptions = 'extra-gen-snapshot-options'; | |
| static const kEnableExperiment = 'enable-experiment'; | |
| static const kFileSystemRoot = 'filesystem-root'; | |
| static const kFileSystemScheme = 'filesystem-scheme'; | |
| static const kSplitDebugInfoOption = 'split-debug-info'; | |
| static const kDartObfuscationOption = 'obfuscate'; | |
| static const kDartDefinesOption = 'dart-define'; | |
| static const kDartDefineFromFileOption = 'dart-define-from-file'; | |
| static const kWebDefinesOption = 'web-define'; | |
| static const kPerformanceMeasurementFile = 'performance-measurement-file'; | |
| static const kDeviceUser = 'device-user'; | |
| static const kDeviceTimeout = 'device-timeout'; | |
| static const kDeviceConnection = 'device-connection'; | |
| static const kAnalyzeSize = 'analyze-size'; | |
| static const kCodeSizeDirectory = 'code-size-directory'; | |
| static const kAndroidGradleDaemon = 'android-gradle-daemon'; | |
| static const kDeferredComponents = 'deferred-components'; | |
| static const kAndroidProjectArgs = 'android-project-arg'; | |
| static const kAndroidGradleProjectCacheDir = 'android-project-cache-dir'; | |
| static const kAndroidSkipBuildDependencyValidation = 'android-skip-build-dependency-validation'; | |
| static const kInitializeFromDill = 'initialize-from-dill'; | |
| static const kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date'; | |
| static const kNativeAssetsYamlFile = 'native-assets-yaml-file'; | |
| static const kFatalWarnings = 'fatal-warnings'; | |
| static const kUseApplicationBinary = 'use-application-binary'; | |
| static const kWebBrowserFlag = 'web-browser-flag'; | |
| static const kWebResourcesCdnFlag = 'web-resources-cdn'; | |
| static const kWebWasmFlag = 'wasm'; | |
| static const kWebExperimentalHotReload = 'web-experimental-hot-reload'; | |
| static const kEnableImpeller = 'enable-impeller'; | |
| static const kCodesignIdentity = 'codesign-identity'; | |
| static const kCodesign = 'codesign'; | |
| static const kAdbLogFiltering = 'adb-log-filtering'; | |
| } | |
| /// flutter command categories for usage. | |
| abstract final class FlutterCommandCategory { | |
| static const sdk = 'Flutter SDK'; | |
| static const project = 'Project'; | |
| static const tools = 'Tools & Devices'; | |
| } | |
| abstract class FlutterCommand extends Command<void> { | |
| FlutterCommand({this.verboseHelp = false, ToolContext? toolContext, this._outputPreferences}) | |
| : _explicitToolContext = toolContext; | |
| /// Whether this command was invoked with verbose help enabled. | |
| final bool verboseHelp; | |
| final ToolContext? _explicitToolContext; | |
| final OutputPreferences? _outputPreferences; | |
| /// The [ToolContext] providing explicit dependency injection for this command. | |
| ToolContext? get toolContext => _explicitToolContext ?? runner?.toolContext; | |
| SystemClock get _clock => toolContext?.systemClock ?? globals.systemClock; | |
| Logger get _logger => toolContext?.logger ?? globals.logger; | |
| Signals get _signals => toolContext?.signals ?? globals.signals; | |
| UserMessages get _userMessages => toolContext?.userMessages ?? globals.userMessages; | |
| PreRunValidator get _preRunValidator => toolContext?.preRunValidator ?? globals.preRunValidator; | |
| OperatingSystemUtils get _os => toolContext?.os ?? globals.os; | |
| PersistentToolState? get _persistentToolState => | |
| toolContext?.persistentToolState ?? globals.persistentToolState; | |
| Platform get _platform => toolContext?.platform ?? globals.platform; | |
| FileSystem get _fs => toolContext?.fs ?? globals.fs; | |
| FlutterProjectFactory get _projectFactory => | |
| toolContext?.projectFactory ?? globals.projectFactory; | |
| Analytics get _analytics => runner?.analytics ?? globals.analytics; | |
| Cache get _cache => toolContext?.cache ?? globals.cache; | |
| FlutterVersion get _flutterVersion => toolContext?.flutterVersion ?? globals.flutterVersion; | |
| FileSystemUtils get _fsUtils => toolContext?.fileSystemUtils ?? globals.fsUtils; | |
| /// The currently executing command (or sub-command). | |
| /// | |
| /// Will be `null` until the top-most command has begun execution. | |
| static FlutterCommand? get current => context.get<FlutterCommand>(); | |
| /// The option name for a custom VM Service port. | |
| static const vmServicePortOption = 'vm-service-port'; | |
| /// The option name for a custom DevTools server address. | |
| static const kDevToolsServerAddress = 'devtools-server-address'; | |
| /// The flag name for whether to launch the DevTools or not. | |
| static const kEnableDevTools = 'devtools'; | |
| /// The flag name for whether or not to use ipv6. | |
| static const ipv6Flag = 'ipv6'; | |
| /// The dart define used for adding the Flutter version at runtime. | |
| @visibleForTesting | |
| static const flutterVersionDefine = 'FLUTTER_VERSION'; | |
| /// The dart define used for adding the Flutter channel at runtime. | |
| @visibleForTesting | |
| static const flutterChannelDefine = 'FLUTTER_CHANNEL'; | |
| /// The dart define used for adding the Flutter git URL at runtime. | |
| @visibleForTesting | |
| static const flutterGitUrlDefine = 'FLUTTER_GIT_URL'; | |
| /// The dart define used for adding the Flutter framework revision at runtime. | |
| @visibleForTesting | |
| static const flutterFrameworkRevisionDefine = 'FLUTTER_FRAMEWORK_REVISION'; | |
| /// The dart define used for adding the Flutter engine revision at runtime. | |
| @visibleForTesting | |
| static const flutterEngineRevisionDefine = 'FLUTTER_ENGINE_REVISION'; | |
| /// The dart define used for adding the Dart version at runtime. | |
| @visibleForTesting | |
| static const flutterDartVersionDefine = 'FLUTTER_DART_VERSION'; | |
| /// List of all dart defines used for adding Flutter version information at runtime | |
| @visibleForTesting | |
| static const flutterVersionDartDefines = <String>[ | |
| flutterVersionDefine, | |
| flutterChannelDefine, | |
| flutterGitUrlDefine, | |
| flutterFrameworkRevisionDefine, | |
| flutterEngineRevisionDefine, | |
| flutterDartVersionDefine, | |
| ]; | |
| @override | |
| ArgParser get argParser => _argParser ??= ArgParser(usageLineLength: _usageLineLength); | |
| ArgParser? _argParser; | |
| int? get _usageLineLength { | |
| final OutputPreferences? prefs = _outputPreferences ?? toolContext?.outputPreferences; | |
| if (prefs != null) { | |
| return prefs.wrapText ? prefs.wrapColumn : null; | |
| } | |
| return null; | |
| } | |
| @override | |
| FlutterCommandRunner? get runner => super.runner as FlutterCommandRunner?; | |
| var _requiresPubspecYaml = false; | |
| /// Whether this command uses the 'target' option. | |
| var _usesTargetOption = false; | |
| /// Enables the target option flag behavior on this command. | |
| void enableUsesTargetOption() { | |
| _usesTargetOption = true; | |
| } | |
| var _usesPubOption = false; | |
| /// Enables the pub option flag behavior on this command. | |
| void enableUsesPubOption() { | |
| _usesPubOption = true; | |
| } | |
| var _usesPortOption = false; | |
| var _usesIpv6Flag = false; | |
| var _usesFatalWarnings = false; | |
| DeprecationBehavior get deprecationBehavior => DeprecationBehavior.none; | |
| bool get shouldRunPub => _usesPubOption && getValue(CommonOptions.pub); | |
| bool get outputMachineFormat => | |
| argParser.options.containsKey(FlutterGlobalOptions.kMachineFlag) && | |
| boolArg(FlutterGlobalOptions.kMachineFlag); | |
| bool get shouldUpdateCache => true; | |
| bool get deprecated => false; | |
| ProcessInfo get processInfo => toolContext?.processInfo ?? globals.processInfo; | |
| /// When the command runs and this is true, trigger an async process to | |
| /// discover devices from discoverers that support wireless devices for an | |
| /// extended amount of time and refresh the device cache with the results. | |
| bool get refreshWirelessDevices => false; | |
| @override | |
| bool get hidden => deprecated; | |
| var _excludeDebug = false; | |
| var _excludeRelease = false; | |
| /// Grabs the [Analytics] instance from the global context. It is defined | |
| /// at the [FlutterCommand] level to enable any classes that extend it to | |
| /// easily reference it or overwrite as necessary. | |
| Analytics get analytics => _analytics; | |
| /// Registers an [OptionBundle] with this command. | |
| void registerOptionBundle(OptionBundle bundle) { | |
| bundle.register(this, argParser); | |
| } | |
| /// Registers multiple [OptionBundle] instances with this command. | |
| void registerOptionBundles(List<OptionBundle> bundles) => bundles.forEach(registerOptionBundle); | |
| void requiresPubspecYaml() { | |
| _requiresPubspecYaml = true; | |
| } | |
| void usesWebOptions({required bool verboseHelp}) { | |
| argParser.addDescriptors(const <OptionDescriptor<Object?>>[ | |
| WebOptions.webHeader, | |
| WebOptions.webHostname, | |
| WebOptions.webPort, | |
| WebOptions.webTlsCertPath, | |
| WebOptions.webTlsCertKeyPath, | |
| WebOptions.webServerDebugProtocol, | |
| WebOptions.webServerDebugBackendProtocol, | |
| WebOptions.webServerDebugInjectedClientProtocol, | |
| WebOptions.webAllowExposeUrl, | |
| WebOptions.webRunHeadless, | |
| WebOptions.webBrowserDebugPort, | |
| WebOptions.webEnableExpressionEvaluation, | |
| WebOptions.webLaunchUrl, | |
| WebOptions.webBrowserFlags, | |
| WebOptions.crossOriginIsolation, | |
| ], verboseHelp: verboseHelp); | |
| usesBaseHrefOption(); | |
| } | |
| void usesBaseHrefOption() { | |
| argParser.addDescriptor(WebOptions.baseHref); | |
| } | |
| void usesTargetOption() { | |
| CommonOptions.target.addTo(argParser); | |
| _usesTargetOption = true; | |
| } | |
| void usesFatalWarningsOption({required bool verboseHelp}) { | |
| argParser.addFlag( | |
| FlutterOptions.kFatalWarnings, | |
| hide: !verboseHelp, | |
| help: | |
| 'Causes the command to fail if warnings are sent to the console ' | |
| 'during its execution.', | |
| ); | |
| _usesFatalWarnings = true; | |
| } | |
| String get targetFile { | |
| if (wasParsed(CommonOptions.target)) { | |
| return getValue(CommonOptions.target); | |
| } | |
| final List<String>? rest = argResults?.rest; | |
| if (rest != null && rest.isNotEmpty) { | |
| return rest.first; | |
| } | |
| return toolContext?.fs.path.join('lib', 'main.dart') ?? bundle.defaultMainPath; | |
| } | |
| /// Indicates if the current command running has a terminal attached. | |
| bool get hasTerminal => toolContext?.stdio.hasTerminal ?? globals.stdio.hasTerminal; | |
| /// Path to the Dart's package config file. | |
| /// | |
| /// This can be overridden by some of its subclasses. | |
| String? get packagesPath => stringArg(FlutterGlobalOptions.kPackagesOption, global: true); | |
| /// Whether flutter is being run from our CI. | |
| /// | |
| /// This is true if `--ci` is passed to the command or if environment | |
| /// variable `LUCI_CI` is `True`. | |
| bool get usingCISystem { | |
| return getValue(CommonOptions.ci) || (_platform.environment['LUCI_CI'] == 'True'); | |
| } | |
| String? get debugLogsDirectoryPath => | |
| stringArg(FlutterGlobalOptions.kDebugLogsDirectoryFlag, global: true); | |
| /// The value of the `--filesystem-scheme` argument. | |
| /// | |
| /// This can be overridden by some of its subclasses. | |
| String? get fileSystemScheme => argParser.options.containsKey(FlutterOptions.kFileSystemScheme) | |
| ? stringArg(FlutterOptions.kFileSystemScheme) | |
| : null; | |
| /// The values of the `--filesystem-root` argument. | |
| /// | |
| /// This can be overridden by some of its subclasses. | |
| List<String>? get fileSystemRoots => argParser.options.containsKey(FlutterOptions.kFileSystemRoot) | |
| ? stringsArg(FlutterOptions.kFileSystemRoot) | |
| : null; | |
| void usesPubOption({bool hide = false}) { | |
| CommonOptions.pub.addTo(argParser, hideOverride: hide); | |
| _usesPubOption = true; | |
| } | |
| /// Adds flags for using a specific filesystem root and scheme. | |
| /// | |
| /// The `hide` argument indicates whether or not to hide these options when | |
| /// the user asks for help. | |
| void usesFilesystemOptions({required bool hide}) { | |
| argParser | |
| ..addOption( | |
| 'output-dill', | |
| hide: hide, | |
| help: 'Specify the path to frontend server output kernel file.', | |
| ) | |
| ..addMultiOption( | |
| FlutterOptions.kFileSystemRoot, | |
| hide: hide, | |
| help: | |
| 'Specify the path that is used as the root of a virtual file system ' | |
| 'during compilation. The input file name should be specified as a URL ' | |
| 'using the scheme given in "--${FlutterOptions.kFileSystemScheme}".\n' | |
| 'Requires the "--output-dill" option to be explicitly specified.', | |
| ) | |
| ..addOption( | |
| FlutterOptions.kFileSystemScheme, | |
| defaultsTo: 'org-dartlang-root', | |
| hide: hide, | |
| help: | |
| 'Specify the scheme that is used for virtual file system used in ' | |
| 'compilation. See also the "--${FlutterOptions.kFileSystemRoot}" option.', | |
| ); | |
| } | |
| /// Adds options for connecting to the Dart VM Service port. | |
| void usesPortOptions({required bool verboseHelp}) { | |
| argParser.addOption( | |
| vmServicePortOption, | |
| help: | |
| '(deprecated; use host-vmservice-port instead) ' | |
| 'Listen to the given port for a Dart VM Service connection.\n' | |
| 'Specifying port 0 (the default) will find a random free port.\n ' | |
| 'if the Dart Development Service (DDS) is enabled, this will not be the port ' | |
| 'of the VmService instance advertised on the command line.', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addOption( | |
| 'device-vmservice-port', | |
| help: | |
| 'Look for vmservice connections only from the specified port.\n' | |
| 'Specifying port 0 (the default) will accept the first vmservice ' | |
| 'discovered.', | |
| ); | |
| argParser.addOption( | |
| 'host-vmservice-port', | |
| help: | |
| 'When a device-side vmservice port is forwarded to a host-side ' | |
| 'port, use this value as the host port.\nSpecifying port 0 ' | |
| '(the default) will find a random free host port.', | |
| ); | |
| _usesPortOption = true; | |
| } | |
| /// Add option values for output directory of artifacts | |
| void usesOutputDir() { | |
| CommonOptions.outputDir.addTo(argParser); | |
| } | |
| void addDevToolsOptions({required bool verboseHelp, bool includeEnableDevTools = true}) { | |
| if (includeEnableDevTools) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.enableDevTools, verboseHelp: verboseHelp); | |
| } | |
| argParser.addDescriptor( | |
| DebuggingOptionDescriptors.devToolsServerAddress( | |
| includeEnableDevTools: includeEnableDevTools, | |
| ), | |
| verboseHelp: verboseHelp, | |
| ); | |
| } | |
| void addDdsOptions({required bool verboseHelp}) { | |
| argParser.addDescriptors(const <OptionDescriptor<Object?>>[ | |
| DebuggingOptionDescriptors.ddsPort, | |
| DebuggingOptionDescriptors.dds, | |
| DebuggingOptionDescriptors.disableDds, | |
| ], verboseHelp: verboseHelp); | |
| } | |
| late final bool enableDds = getValue(DebuggingOptionDescriptors.dds); | |
| bool get _hostVmServicePortProvided => | |
| (argResults?.wasParsed(vmServicePortOption) ?? false) || | |
| (argResults?.wasParsed('host-vmservice-port') ?? false); | |
| int _tryParseHostVmservicePort() { | |
| final String? vmServicePort = stringArg(vmServicePortOption); | |
| final String? hostPort = stringArg('host-vmservice-port'); | |
| if (vmServicePort == null && hostPort == null) { | |
| throwToolExit('Invalid port for `--vm-service-port/--host-vmservice-port`'); | |
| } | |
| try { | |
| return int.parse((vmServicePort ?? hostPort)!); | |
| } on FormatException catch (error) { | |
| throwToolExit('Invalid port for `--vm-service-port/--host-vmservice-port`: $error'); | |
| } | |
| } | |
| int get ddsPort { | |
| if (!wasParsed(DebuggingOptionDescriptors.ddsPort) && _hostVmServicePortProvided) { | |
| // If an explicit DDS port is _not_ provided, use the host-vmservice-port for DDS. | |
| return _tryParseHostVmservicePort(); | |
| } | |
| return getValue(DebuggingOptionDescriptors.ddsPort); | |
| } | |
| Uri? get devToolsServerAddress { | |
| if (wasParsed(DebuggingOptionDescriptors.devToolsServerAddressOption)) { | |
| final Uri? uri = Uri.tryParse( | |
| getValue(DebuggingOptionDescriptors.devToolsServerAddressOption)!, | |
| ); | |
| if (uri != null && uri.host.isNotEmpty && uri.port != 0) { | |
| return uri; | |
| } | |
| } | |
| return null; | |
| } | |
| /// Gets the vmservice port provided to in the 'vm-service-port' or | |
| /// 'host-vmservice-port option. | |
| /// | |
| /// Only one of "host-vmservice-port" and "vm-service-port" may be | |
| /// specified. | |
| /// | |
| /// If no port is set, returns null. | |
| int? get hostVmservicePort { | |
| if (!_usesPortOption || !_hostVmServicePortProvided) { | |
| return null; | |
| } | |
| if ((argResults?.wasParsed(vmServicePortOption) ?? false) && | |
| (argResults?.wasParsed('host-vmservice-port') ?? false)) { | |
| throwToolExit( | |
| 'Only one of "--vm-service-port" and ' | |
| '"--host-vmservice-port" may be specified.', | |
| ); | |
| } | |
| // If DDS is enabled and no explicit DDS port is provided, use the | |
| // host-vmservice-port for DDS instead and bind the VM service to a random | |
| // port. | |
| if (enableDds && !wasParsed(DebuggingOptionDescriptors.ddsPort)) { | |
| return null; | |
| } | |
| return _tryParseHostVmservicePort(); | |
| } | |
| /// Gets the vmservice port provided to in the 'device-vmservice-port' option. | |
| /// | |
| /// If no port is set, returns null. | |
| int? get deviceVmservicePort { | |
| final String? devicePort = stringArg('device-vmservice-port'); | |
| if (!_usesPortOption || devicePort == null) { | |
| return null; | |
| } | |
| try { | |
| return int.parse(devicePort); | |
| } on FormatException catch (error) { | |
| throwToolExit('Invalid port for `--device-vmservice-port`: $error'); | |
| } | |
| } | |
| void addPublishPort({bool enabledByDefault = true, bool verboseHelp = false}) { | |
| argParser.addDescriptor( | |
| DebuggingOptionDescriptors.publishPortOption(enabledByDefault: enabledByDefault), | |
| verboseHelp: verboseHelp, | |
| ); | |
| } | |
| Future<bool> get disablePortPublication async => | |
| !getValue(DebuggingOptionDescriptors.publishPort); | |
| void usesIpv6Flag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.ipv6, verboseHelp: verboseHelp); | |
| _usesIpv6Flag = true; | |
| } | |
| bool? get ipv6 => _usesIpv6Flag ? getValue(DebuggingOptionDescriptors.ipv6) : null; | |
| void usesBuildNumberOption() { | |
| CommonOptions.buildNumber.addTo(argParser); | |
| } | |
| void usesBuildNameOption() { | |
| CommonOptions.buildName.addTo(argParser); | |
| } | |
| void usesDartDefineOption() { | |
| CommonOptions.dartDefines.addTo(argParser); | |
| _usesDartDefineFromFileOption(); | |
| } | |
| void usesWebDefineOption() { | |
| argParser.addMultiOption( | |
| FlutterOptions.kWebDefinesOption, | |
| help: | |
| 'Additional key-value pairs that will be available as template variables ' | |
| 'in web/index.html and web/flutter_bootstrap.js files during development and build.\n' | |
| 'Variables are replaced in the format {{VARIABLE_NAME}}.\n' | |
| 'Multiple defines can be passed by repeating "--${FlutterOptions.kWebDefinesOption}" multiple times.\n' | |
| 'If a template contains a variable placeholder but no corresponding "--web-define" is provided, ' | |
| 'it will warn that you have an unhandled variable.', | |
| valueHelp: 'API_URL=https://api.example.com', | |
| splitCommas: false, | |
| ); | |
| } | |
| void _usesDartDefineFromFileOption() { | |
| CommonOptions.dartDefineFromFile.addTo(argParser); | |
| } | |
| void usesWebResourcesCdnFlag() { | |
| argParser.addFlag( | |
| FlutterOptions.kWebResourcesCdnFlag, | |
| defaultsTo: true, | |
| help: 'Use Web static resources hosted on a CDN.', | |
| ); | |
| } | |
| void usesDeviceUserOption() { | |
| argParser.addOption( | |
| FlutterOptions.kDeviceUser, | |
| help: 'Identifier number for a user or work profile on Android only. Run "adb shell pm list users" for available identifiers.', | |
| valueHelp: '10', | |
| ); | |
| } | |
| void usesDeviceTimeoutOption() { | |
| argParser.addOption( | |
| FlutterOptions.kDeviceTimeout, | |
| help: 'Time in seconds to wait for devices to attach. Longer timeouts may be necessary for networked devices.', | |
| valueHelp: '10', | |
| ); | |
| } | |
| void usesDeviceConnectionOption() { | |
| argParser.addOption( | |
| FlutterOptions.kDeviceConnection, | |
| defaultsTo: 'both', | |
| help: 'Discover devices based on connection type.', | |
| allowed: <String>['attached', 'wireless', 'both'], | |
| allowedHelp: <String, String>{ | |
| 'both': 'Searches for both attached and wireless devices.', | |
| 'attached': 'Only searches for devices connected by USB or built-in (such as simulators/emulators, MacOS/Windows, Chrome)', | |
| 'wireless': 'Only searches for devices connected wirelessly. Discovering wireless devices may take longer.', | |
| }, | |
| ); | |
| } | |
| void usesAdbLogFilteringOption({required bool hide}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.adbLogFiltering, hideOverride: hide); | |
| } | |
| void usesApplicationBinaryOption() { | |
| argParser.addOption( | |
| FlutterOptions.kUseApplicationBinary, | |
| help: | |
| 'Specify a pre-built application binary to use when running. For Android applications, ' | |
| 'this must be the path to an APK. For iOS applications, the path to an IPA. Other device types ' | |
| 'do not yet support prebuilt application binaries.', | |
| valueHelp: 'path/to/app.apk', | |
| ); | |
| } | |
| /// Whether it is safe for this command to use a cached pub invocation. | |
| bool get cachePubGet => true; | |
| late final Duration? deviceDiscoveryTimeout = () { | |
| if ((argResults?.options.contains(FlutterOptions.kDeviceTimeout) ?? false) && | |
| (argResults?.wasParsed(FlutterOptions.kDeviceTimeout) ?? false)) { | |
| final int? timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout)!); | |
| if (timeoutSeconds == null) { | |
| throwToolExit( | |
| 'Could not parse "--${FlutterOptions.kDeviceTimeout}" argument. It must be an integer.', | |
| ); | |
| } | |
| return Duration(seconds: timeoutSeconds); | |
| } | |
| return null; | |
| }(); | |
| DeviceConnectionInterface? get deviceConnectionInterface { | |
| if ((argResults?.options.contains(FlutterOptions.kDeviceConnection) ?? false) && | |
| (argResults?.wasParsed(FlutterOptions.kDeviceConnection) ?? false)) { | |
| return switch (stringArg(FlutterOptions.kDeviceConnection)) { | |
| 'attached' => DeviceConnectionInterface.attached, | |
| 'wireless' => DeviceConnectionInterface.wireless, | |
| _ => null, | |
| }; | |
| } | |
| return null; | |
| } | |
| late final _targetDevices = TargetDevices( | |
| platform: _platform, | |
| deviceManager: globals.deviceManager!, | |
| logger: _logger, | |
| deviceConnectionInterface: deviceConnectionInterface, | |
| ); | |
| void addBuildModeFlags({ | |
| required bool verboseHelp, | |
| bool defaultToRelease = true, | |
| bool excludeDebug = false, | |
| bool excludeRelease = false, | |
| }) { | |
| // A release build must be the default if a debug build is not possible. | |
| assert(defaultToRelease || !excludeDebug); | |
| _excludeDebug = excludeDebug; | |
| _excludeRelease = excludeRelease; | |
| defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug; | |
| if (!excludeDebug) { | |
| argParser.addFlag( | |
| 'debug', | |
| negatable: false, | |
| help: 'Build a debug version of your app${defaultToRelease ? '' : ' (default mode)'}.', | |
| ); | |
| } | |
| argParser.addFlag( | |
| 'profile', | |
| negatable: false, | |
| help: 'Build a version of your app specialized for performance profiling.', | |
| ); | |
| if (!excludeRelease) { | |
| argParser.addFlag( | |
| 'release', | |
| negatable: false, | |
| help: 'Build a release version of your app${defaultToRelease ? ' (default mode)' : ''}.', | |
| ); | |
| argParser.addFlag( | |
| 'jit-release', | |
| negatable: false, | |
| hide: !verboseHelp, | |
| help: | |
| 'Build a JIT release version of your app${defaultToRelease ? ' (default mode)' : ''}.', | |
| ); | |
| } | |
| } | |
| void addSplitDebugInfoOption() { | |
| BuildInfoOptions.splitDebugInfo.addTo(argParser); | |
| } | |
| void addDartObfuscationOption() { | |
| BuildInfoOptions.obfuscate.addTo(argParser); | |
| } | |
| void addTreeShakeIconsFlag({bool? enabledByDefault}) { | |
| CommonOptions.treeShakeIcons.addTo(argParser, hideOverride: enabledByDefault == false); | |
| } | |
| void usesFrontendServerStarterPathOption({required bool verboseHelp}) { | |
| BuildInfoOptions.frontendServerStarterPath.addTo(argParser, verboseHelp: verboseHelp); | |
| } | |
| /// Enables support for the hidden options --extra-front-end-options and | |
| /// --extra-gen-snapshot-options. | |
| void usesExtraDartFlagOptions({required bool verboseHelp}) { | |
| BuildInfoOptions.extraFrontEndOptions.addTo(argParser, verboseHelp: verboseHelp); | |
| BuildInfoOptions.extraGenSnapshotOptions.addTo(argParser, verboseHelp: verboseHelp); | |
| } | |
| void usesFuchsiaOptions({bool hide = false}) { | |
| argParser.addOption( | |
| 'target-model', | |
| help: 'Target model that determines what core libraries are available.', | |
| defaultsTo: 'flutter', | |
| hide: hide, | |
| allowed: const <String>['flutter', 'flutter_runner'], | |
| ); | |
| argParser.addOption( | |
| 'module', | |
| abbr: 'm', | |
| hide: hide, | |
| help: 'The name of the module (required if attaching to a fuchsia device).', | |
| valueHelp: 'module-name', | |
| ); | |
| } | |
| void addEnableExperimentation({required bool hide}) { | |
| CommonOptions.enableExperiment.addTo(argParser, hideOverride: hide); | |
| } | |
| void addBuildPerformanceFile({bool hide = false}) { | |
| BuildInfoOptions.performanceMeasurementFile.addTo(argParser, hideOverride: hide); | |
| } | |
| void addAndroidSpecificBuildOptions({bool hide = false}) { | |
| BuildInfoOptions.androidGradleDaemon.addTo(argParser, hideOverride: hide); | |
| BuildInfoOptions.androidSkipBuildDependencyValidation.addTo(argParser, hideOverride: hide); | |
| BuildInfoOptions.androidProjectArg.addTo(argParser, hideOverride: hide); | |
| BuildInfoOptions.androidProjectCacheDir.addTo(argParser, hideOverride: hide); | |
| } | |
| void addNativeNullAssertions({bool hide = false}) { | |
| CommonOptions.nativeNullAssertions.addTo(argParser, hideOverride: hide); | |
| } | |
| void usesInitializeFromDillOption({required bool hide}) { | |
| BuildInfoOptions.initializeFromDill.addTo(argParser, hideOverride: hide); | |
| BuildInfoOptions.assumeInitializeFromDillUpToDate.addTo(argParser, hideOverride: hide); | |
| } | |
| void usesNativeAssetsOption({required bool hide}) { | |
| argParser.addOption( | |
| FlutterOptions.kNativeAssetsYamlFile, | |
| help: | |
| 'Initializes the resident compiler with a custom native assets ' | |
| 'yaml file instead of the default cached location.', | |
| hide: hide, | |
| ); | |
| } | |
| void addIgnoreDeprecationOption({bool hide = false}) { | |
| BuildInfoOptions.ignoreDeprecation.addTo(argParser, hideOverride: hide); | |
| } | |
| /// Adds build options common to all of the desktop build commands. | |
| void addCommonDesktopBuildOptions({required bool verboseHelp}) { | |
| addBuildModeFlags(verboseHelp: verboseHelp); | |
| addBuildPerformanceFile(hide: !verboseHelp); | |
| addDartObfuscationOption(); | |
| addEnableExperimentation(hide: !verboseHelp); | |
| addSplitDebugInfoOption(); | |
| addTreeShakeIconsFlag(); | |
| usesAnalyzeSizeFlag(); | |
| usesDartDefineOption(); | |
| usesExtraDartFlagOptions(verboseHelp: verboseHelp); | |
| usesPubOption(); | |
| usesTargetOption(); | |
| usesTrackWidgetCreation(verboseHelp: verboseHelp); | |
| usesBuildNumberOption(); | |
| usesBuildNameOption(); | |
| } | |
| /// The build mode that this command will use if no build mode is | |
| /// explicitly specified. | |
| /// | |
| /// Use [getBuildMode] to obtain the actual effective build mode. | |
| BuildMode defaultBuildMode = BuildMode.debug; | |
| BuildMode getBuildMode() { | |
| // No debug when _excludeDebug is true. If debug is not excluded, then take | |
| // the command line flag (if such exists for this command). | |
| bool argIfDefined(String flagName, bool ifNotDefined) { | |
| return argParser.options.containsKey(flagName) ? boolArg(flagName) : ifNotDefined; | |
| } | |
| final bool debugResult = !_excludeDebug && argIfDefined('debug', false); | |
| final bool jitReleaseResult = !_excludeRelease && argIfDefined('jit-release', false); | |
| final bool releaseResult = !_excludeRelease && argIfDefined('release', false); | |
| final bool profileResult = argIfDefined('profile', false); | |
| final modeFlags = <bool>[debugResult, profileResult, jitReleaseResult, releaseResult]; | |
| if (modeFlags.where((bool flag) => flag).length > 1) { | |
| throw UsageException( | |
| 'Only one of "--debug", "--profile", "--jit-release", ' | |
| 'or "--release" can be specified.', | |
| '', | |
| ); | |
| } | |
| if (debugResult) { | |
| return BuildMode.debug; | |
| } | |
| if (profileResult) { | |
| return BuildMode.profile; | |
| } | |
| if (releaseResult) { | |
| return BuildMode.release; | |
| } | |
| if (jitReleaseResult) { | |
| return BuildMode.jitRelease; | |
| } | |
| return defaultBuildMode; | |
| } | |
| void usesFlavorOption() { | |
| BuildInfoOptions.flavor.addTo(argParser); | |
| } | |
| void usesTrackWidgetCreation({bool hasEffect = true, required bool verboseHelp}) { | |
| BuildInfoOptions.trackWidgetCreation.addTo( | |
| argParser, | |
| verboseHelp: verboseHelp, | |
| hideOverride: !hasEffect && !verboseHelp, | |
| ); | |
| } | |
| void usesAnalyzeSizeFlag() { | |
| BuildInfoOptions.analyzeSize.addTo(argParser); | |
| BuildInfoOptions.codeSizeDirectory.addTo(argParser); | |
| } | |
| void addEnableImpellerFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.enableImpeller, verboseHelp: verboseHelp); | |
| } | |
| void addEnableFlutterGpuFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.enableFlutterGpu, verboseHelp: verboseHelp); | |
| } | |
| void addEnableVulkanValidationFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor( | |
| DebuggingOptionDescriptors.enableVulkanValidation, | |
| verboseHelp: verboseHelp, | |
| ); | |
| } | |
| void addEnableEmbedderApiFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.enableEmbedderApi, verboseHelp: verboseHelp); | |
| } | |
| void addMachineOutputFlag({required bool verboseHelp}) { | |
| argParser.addFlag( | |
| FlutterGlobalOptions.kMachineFlag, | |
| negatable: false, | |
| help: 'Outputs in a machine readable structured JSON format.', | |
| hide: !verboseHelp, | |
| ); | |
| } | |
| void addEnableHcppFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.enableHcpp, verboseHelp: verboseHelp); | |
| } | |
| /// The explicit `--[no-]enable-hcpp` value, or null when the flag was not | |
| /// passed (or the command does not define it). | |
| /// | |
| /// This takes priority over the `io.flutter.embedding.android.EnableHcpp` | |
| /// manifest entry: it is passed to Gradle, which writes it into the merged | |
| /// manifest over any value already there. Commands that launch the app | |
| /// (run/test/drive) additionally forward it to the device. | |
| bool? get explicitEnableHcpp { | |
| if (!hasOption(DebuggingOptionDescriptors.enableHcpp) || | |
| !wasParsed(DebuggingOptionDescriptors.enableHcpp)) { | |
| return null; | |
| } | |
| return getValue(DebuggingOptionDescriptors.enableHcpp); | |
| } | |
| /// The HCPP value for an Android artifact when the developer did not pass | |
| /// `--[no-]enable-hcpp`: the `enable-hcpp` feature flag, which is on by | |
| /// default on master and beta. | |
| /// | |
| /// This is only a default. Gradle injects it when the merged manifest does | |
| /// not set `io.flutter.embedding.android.EnableHcpp` at all, so an entry in | |
| /// the manifest wins over it. [explicitEnableHcpp] in turn wins over both. | |
| bool get enableHcpp => explicitEnableHcpp ?? runner?.featureFlags?.isHcppEnabled ?? false; | |
| void addTestFlag({required bool verboseHelp}) { | |
| argParser.addDescriptor(DebuggingOptionDescriptors.testFlag, verboseHelp: verboseHelp); | |
| } | |
| /// Returns a [FlutterProject] view of the current directory or a ToolExit error, | |
| /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. | |
| FlutterProject get project => _projectFactory.fromDirectory(_fs.currentDirectory); | |
| /// The path to the package config for the current project. | |
| /// | |
| /// If an explicit argument is given, that is returned. Otherwise the file | |
| /// system is searched for the package config. For projects in pub workspaces | |
| /// the package config might be located in a parent directory. | |
| /// | |
| /// If none is found `.dart_tool/package_config.json` is returned. | |
| String packageConfigPath() { | |
| final String? packagesPath = this.packagesPath; | |
| return packagesPath ?? findPackageConfigFileOrDefault(project.directory).path; | |
| } | |
| /// Compute the [BuildInfo] for the current flutter command. | |
| /// | |
| /// Commands that build multiple build modes can pass in a [forcedBuildMode] | |
| /// to be used instead of parsing flags. | |
| /// | |
| /// Throws a [ToolExit] if the current set of options is not compatible with | |
| /// each other. | |
| Future<BuildInfo> getBuildInfo({ | |
| BuildMode? forcedBuildMode, | |
| File? forcedTargetFile, | |
| bool? forcedUseLocalCanvasKit, | |
| // TODO(nshahan): Delete when fully migrated to new module system, | |
| // https://github.com/flutter/flutter/issues/142060. | |
| bool? forcedWebEnableHotReload, | |
| }) async { | |
| final bool trackWidgetCreation = | |
| hasOption(BuildInfoOptions.trackWidgetCreation) && | |
| getValue(BuildInfoOptions.trackWidgetCreation); | |
| final String? buildNumber = getValue(CommonOptions.buildNumber); | |
| final String? buildName = getValue(CommonOptions.buildName); | |
| final File packageConfigFile = _fs.file(packageConfigPath()); | |
| final PackageConfig packageConfig = await loadPackageConfigWithLogging( | |
| packageConfigFile, | |
| logger: _logger, | |
| throwOnError: false, | |
| ); | |
| final List<String> experiments = getValue(CommonOptions.enableExperiment); | |
| final List<String> extraGenSnapshotOptions = getValue(BuildInfoOptions.extraGenSnapshotOptions) | |
| .toList(); | |
| final List<String> extraFrontEndOptions = getValue(BuildInfoOptions.extraFrontEndOptions) | |
| .toList(); | |
| if (experiments.isNotEmpty) { | |
| for (final expFlag in experiments) { | |
| final flag = '--enable-experiment=$expFlag'; | |
| extraFrontEndOptions.add(flag); | |
| extraGenSnapshotOptions.add(flag); | |
| } | |
| } | |
| String? codeSizeDirectory; | |
| if (getValue(BuildInfoOptions.analyzeSize)) { | |
| final String? customDir = getValue(BuildInfoOptions.codeSizeDirectory); | |
| final Directory directory = (customDir != null) | |
| ? _fs.directory(customDir) | |
| : _fsUtils.getUniqueDirectory(_fs.directory(getBuildDirectory()), 'flutter_size'); | |
| directory.createSync(recursive: true); | |
| codeSizeDirectory = directory.path; | |
| } | |
| final bool dartObfuscation = getValue(BuildInfoOptions.obfuscate); | |
| final String? splitDebugInfoPath = getValue(BuildInfoOptions.splitDebugInfo); | |
| final bool androidGradleDaemon = | |
| !hasOption(BuildInfoOptions.androidGradleDaemon) || | |
| getValue(BuildInfoOptions.androidGradleDaemon); | |
| final bool androidSkipBuildDependencyValidation = | |
| !hasOption(BuildInfoOptions.androidSkipBuildDependencyValidation) || | |
| getValue(BuildInfoOptions.androidSkipBuildDependencyValidation); | |
| final List<String> androidProjectArgs = getValue(BuildInfoOptions.androidProjectArg); | |
| final String? androidGradleProjectCacheDir = getValue(BuildInfoOptions.androidProjectCacheDir); | |
| if (dartObfuscation && (splitDebugInfoPath == null || splitDebugInfoPath.isEmpty)) { | |
| throwToolExit( | |
| '"--${FlutterOptions.kDartObfuscationOption}" can only be used in ' | |
| 'combination with "--${FlutterOptions.kSplitDebugInfoOption}"', | |
| ); | |
| } | |
| final BuildMode buildMode = forcedBuildMode ?? getBuildMode(); | |
| if (buildMode != BuildMode.release && codeSizeDirectory != null) { | |
| throwToolExit('"--${FlutterOptions.kAnalyzeSize}" can only be used on release builds.'); | |
| } | |
| if (codeSizeDirectory != null && splitDebugInfoPath != null) { | |
| throwToolExit( | |
| '"--${FlutterOptions.kAnalyzeSize}" cannot be combined with "--${FlutterOptions.kSplitDebugInfoOption}".', | |
| ); | |
| } | |
| final bool treeShakeIcons = | |
| hasOption(CommonOptions.treeShakeIcons) && | |
| buildMode.isPrecompiled && | |
| getValue(CommonOptions.treeShakeIcons); | |
| final String? performanceMeasurementFile = getValue( | |
| BuildInfoOptions.performanceMeasurementFile, | |
| ); | |
| final Map<String, Object?> defineConfigJsonMap = extractDartDefineConfigJsonMap(); | |
| final List<String> dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap); | |
| final bool useCdn = getValue(WebOptions.webResourcesCdn); | |
| var useLocalWebSdk = false; | |
| if (globalResults?.wasParsed(FlutterGlobalOptions.kLocalWebSDKOption) ?? false) { | |
| useLocalWebSdk = stringArg(FlutterGlobalOptions.kLocalWebSDKOption, global: true) != null; | |
| } | |
| final bool useLocalCanvasKit = forcedUseLocalCanvasKit ?? (!useCdn || useLocalWebSdk); | |
| final String? defaultFlavor = project.manifest.defaultFlavor; | |
| final String? cliFlavor = getValue(BuildInfoOptions.flavor); | |
| final String? flavor = cliFlavor ?? defaultFlavor; | |
| _ensureReservedDartDefineIsUnset(kAppFlavor, dartDefines); | |
| if (flavor != null) { | |
| dartDefines.add('$kAppFlavor=$flavor'); | |
| } | |
| for (final (String define, String? value) in <(String, String?)>[ | |
| (kAppBuildName, buildName ?? project.manifest.buildName), | |
| (kAppBuildNumber, buildNumber ?? project.manifest.buildNumber), | |
| ]) { | |
| _ensureReservedDartDefineIsUnset(define, dartDefines); | |
| if (value != null) { | |
| dartDefines.add('$define=$value'); | |
| } | |
| } | |
| _addFlutterVersionToDartDefines(_flutterVersion, dartDefines); | |
| _addFeatureFlagsToDartDefines(dartDefines); | |
| return BuildInfo( | |
| buildMode, | |
| flavor, | |
| trackWidgetCreation: trackWidgetCreation, | |
| frontendServerStarterPath: getValue(BuildInfoOptions.frontendServerStarterPath), | |
| extraFrontEndOptions: extraFrontEndOptions.isNotEmpty ? extraFrontEndOptions : null, | |
| extraGenSnapshotOptions: extraGenSnapshotOptions.isNotEmpty ? extraGenSnapshotOptions : null, | |
| fileSystemRoots: fileSystemRoots, | |
| fileSystemScheme: fileSystemScheme, | |
| buildNumber: buildNumber, | |
| buildName: buildName, | |
| treeShakeIcons: treeShakeIcons, | |
| splitDebugInfoPath: splitDebugInfoPath, | |
| dartObfuscation: dartObfuscation, | |
| dartDefines: dartDefines, | |
| dartExperiments: experiments, | |
| performanceMeasurementFile: performanceMeasurementFile, | |
| packageConfigPath: packagesPath ?? packageConfigFile.path, | |
| codeSizeDirectory: codeSizeDirectory, | |
| androidGradleDaemon: androidGradleDaemon, | |
| androidSkipBuildDependencyValidation: androidSkipBuildDependencyValidation, | |
| androidEnableHcpp: enableHcpp, | |
| explicitAndroidEnableHcpp: explicitEnableHcpp, | |
| packageConfig: packageConfig, | |
| androidProjectArgs: androidProjectArgs, | |
| androidGradleProjectCacheDir: androidGradleProjectCacheDir, | |
| initializeFromDill: getValue(BuildInfoOptions.initializeFromDill), | |
| assumeInitializeFromDillUpToDate: getValue(BuildInfoOptions.assumeInitializeFromDillUpToDate), | |
| useLocalCanvasKit: useLocalCanvasKit, | |
| webEnableHotReload: true, | |
| ); | |
| } | |
| /// Throws a [ToolExit] if [define], a dart-define key reserved by the | |
| /// framework, has been set either in the environment or through | |
| /// `--${FlutterOptions.kDartDefinesOption}` / `--${FlutterOptions.kDartDefineFromFileOption}`. | |
| void _ensureReservedDartDefineIsUnset(String define, List<String> dartDefines) { | |
| if (_platform.environment[define] != null) { | |
| throwToolExit('$define is used by the framework and cannot be set in the environment.'); | |
| } | |
| if (dartDefines.any((String d) => d == define || d.startsWith('$define='))) { | |
| throwToolExit( | |
| '$define is used by the framework and cannot be ' | |
| 'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}', | |
| ); | |
| } | |
| } | |
| // This adds the Dart defines used to access various Flutter version information at runtime. | |
| void _addFlutterVersionToDartDefines(FlutterVersion version, List<String> dartDefines) { | |
| for (final String dartDefine in flutterVersionDartDefines) { | |
| if (dartDefines.any((String define) => define.startsWith(dartDefine))) { | |
| throwToolExit( | |
| '$dartDefine is used by the framework and cannot be ' | |
| 'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}. ' | |
| 'Use FlutterVersion to access it in Flutter code', | |
| ); | |
| } | |
| } | |
| dartDefines.addAll(<String>[ | |
| '$flutterVersionDefine=${version.frameworkVersion}', | |
| '$flutterChannelDefine=${version.channel}', | |
| '$flutterGitUrlDefine=${version.repositoryUrl}', | |
| '$flutterFrameworkRevisionDefine=${version.frameworkRevisionShort}', | |
| '$flutterEngineRevisionDefine=${version.engineRevisionShort}', | |
| '$flutterDartVersionDefine=${version.dartSdkVersion}', | |
| ]); | |
| } | |
| void _addFeatureFlagsToDartDefines(List<String> dartDefines) { | |
| if (dartDefines.any((String define) => define.startsWith(kEnabledFeatureFlags))) { | |
| throwToolExit( | |
| '$kEnabledFeatureFlags is used by the framework and cannot be ' | |
| 'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}.\n' | |
| '\n' | |
| 'Use the "flutter config" command to enable feature flags.', | |
| ); | |
| } | |
| final FeatureFlags? flags = runner?.featureFlags; | |
| if (flags == null) { | |
| return; | |
| } | |
| final String enabledFeatureFlags = flags.allFeatures | |
| .where((Feature feature) => flags.isEnabled(feature)) | |
| .where((Feature feature) => feature.runtimeId != null) | |
| .map((Feature feature) => feature.runtimeId!) | |
| .join(','); | |
| if (enabledFeatureFlags.isNotEmpty) { | |
| dartDefines.add('$kEnabledFeatureFlags=$enabledFeatureFlags'); | |
| } | |
| } | |
| /// The path to send to Google Analytics. Return null here to disable | |
| /// tracking of the command. | |
| Future<String?> get usagePath async { | |
| if (parent is FlutterCommand) { | |
| final commandParent = parent as FlutterCommand?; | |
| final String? path = await commandParent?.usagePath; | |
| // Don't report for parents that return null for usagePath. | |
| return path == null ? null : '$path/$name'; | |
| } else { | |
| return name; | |
| } | |
| } | |
| /// Additional usage values to be sent with the usage ping for | |
| /// package:unified_analytics. | |
| /// | |
| /// Implementations of [FlutterCommand] can override this getter in order | |
| /// to add additional parameters in the [Event.commandUsageValues] constructor. | |
| Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => | |
| Event.commandUsageValues(workflow: commandPath, commandHasTerminal: hasTerminal); | |
| /// Runs this command. | |
| /// | |
| /// Rather than overriding this method, subclasses should override | |
| /// [verifyThenRunCommand] to perform any verification | |
| /// and [runCommand] to execute the command | |
| /// so that this method can record and report the overall time to analytics. | |
| @override | |
| Future<void> run() { | |
| final DateTime startTime = _clock.now(); | |
| return context.run<void>( | |
| name: 'command', | |
| overrides: <Type, Generator>{FlutterCommand: () => this}, | |
| body: () async { | |
| if (_usesFatalWarnings) { | |
| _logger.fatalWarnings = boolArg(FlutterOptions.kFatalWarnings); | |
| } | |
| _printDeprecationWarning(); | |
| final String? commandPath = await usagePath; | |
| if (commandPath != null) { | |
| _registerSignalHandlers(commandPath, startTime); | |
| } | |
| var commandResult = FlutterCommandResult.fail(); | |
| try { | |
| commandResult = await verifyThenRunCommand(commandPath); | |
| } finally { | |
| final DateTime endTime = _clock.now(); | |
| _logger.printTrace( | |
| _userMessages.flutterElapsedTime( | |
| name, | |
| getElapsedAsMilliseconds(endTime.difference(startTime)), | |
| ), | |
| ); | |
| if (commandPath != null) { | |
| _sendPostUsage(commandPath, commandResult, startTime, endTime); | |
| } | |
| if (_usesFatalWarnings) { | |
| _logger.checkForFatalLogs(); | |
| } | |
| } | |
| }, | |
| ); | |
| } | |
| @override | |
| void printUsage() { | |
| _logger.printStatus(usage); | |
| } | |
| @visibleForOverriding | |
| String get deprecationWarning { | |
| return '${_logger.terminal.warningMark} The "$name" command is ' | |
| 'deprecated and will be removed in a future version of Flutter. ' | |
| 'See https://flutter.dev/to/previous-releases ' | |
| 'for previous releases of Flutter.\n'; | |
| } | |
| void _printDeprecationWarning() { | |
| if (deprecated) { | |
| _logger.printWarning(deprecationWarning); | |
| } | |
| } | |
| List<String> extractDartDefines({required Map<String, Object?> defineConfigJsonMap}) { | |
| final dartDefines = <String>[]; | |
| defineConfigJsonMap.forEach((String key, Object? value) { | |
| dartDefines.add('$key=$value'); | |
| }); | |
| if (argParser.options.containsKey(FlutterOptions.kDartDefinesOption)) { | |
| final Iterable<String> defines = stringsArg(FlutterOptions.kDartDefinesOption) | |
| .where((string) => string.isNotEmpty); | |
| dartDefines.addAll(defines); | |
| } | |
| return dartDefines; | |
| } | |
| Map<String, String> extractWebDefines() { | |
| final webDefines = <String, String>{}; | |
| if (argParser.options.containsKey(FlutterOptions.kWebDefinesOption)) { | |
| final List<String> defines = stringsArg(FlutterOptions.kWebDefinesOption); | |
| for (final define in defines) { | |
| final int separatorIndex = define.indexOf('='); | |
| if (separatorIndex == -1 || separatorIndex == 0) { | |
| throwToolExit( | |
| 'Invalid web-define format: $define\n' | |
| 'Expected format: KEY=VALUE (e.g., API_URL=https://api.example.com)', | |
| ); | |
| } | |
| final String key = define.substring(0, separatorIndex); | |
| final String value = define.substring(separatorIndex + 1); | |
| webDefines[key] = value; | |
| } | |
| } | |
| return webDefines; | |
| } | |
| Map<String, Object?> extractDartDefineConfigJsonMap() { | |
| final dartDefineConfigJsonMap = <String, Object?>{}; | |
| if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) { | |
| final List<String> configFilePaths = stringsArg(FlutterOptions.kDartDefineFromFileOption); | |
| for (final path in configFilePaths) { | |
| if (!_fs.isFileSync(path)) { | |
| throwToolExit( | |
| 'Did not find the file passed to "--${FlutterOptions.kDartDefineFromFileOption}". Path: $path', | |
| ); | |
| } | |
| String configRaw; | |
| try { | |
| configRaw = decodeUtf8OrUtf16(_fs.file(path).readAsBytesSync()); | |
| } on Exception catch (err) { | |
| throwToolExit( | |
| 'Unable to decode the file at path "$path". ' | |
| 'Ensure that the file is encoded in UTF-8 or UTF-16.\n' | |
| 'Error details: $err', | |
| ); | |
| } | |
| // Determine whether the file content is JSON or .env format. | |
| String configJsonRaw; | |
| if (configRaw.trim().startsWith('{')) { | |
| configJsonRaw = configRaw; | |
| } else { | |
| // Convert env file to JSON. | |
| configJsonRaw = convertEnvFileToJsonRaw(configRaw); | |
| } | |
| try { | |
| // Fix json convert Object value :type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, Object>' in type cast | |
| (json.decode(configJsonRaw) as Map<String, dynamic>).forEach((String key, Object? value) { | |
| dartDefineConfigJsonMap[key] = value; | |
| }); | |
| } on FormatException catch (err) { | |
| throwToolExit( | |
| 'Unable to parse the file at path "$path" due to a formatting error. ' | |
| 'Ensure that the file contains valid JSON.\n' | |
| 'Error details: $err', | |
| ); | |
| } | |
| } | |
| } | |
| return dartDefineConfigJsonMap; | |
| } | |
| /// Parse a property line from an env file. | |
| /// Supposed property structure should be: | |
| /// key=value | |
| /// | |
| /// Where: key is a string without spaces and value is a string. | |
| /// Value can also contain '=' char. | |
| /// | |
| /// Returns a record of key and value as strings. | |
| MapEntry<String, String> _parseProperty(String line) { | |
| if (DotEnvRegex.multiLineBlock.hasMatch(line)) { | |
| throwToolExit('Multi-line value is not supported: $line'); | |
| } | |
| final Match? keyValueMatch = DotEnvRegex.keyValue.firstMatch(line); | |
| if (keyValueMatch == null) { | |
| throwToolExit( | |
| 'Unable to parse file provided for ' | |
| '--${FlutterOptions.kDartDefineFromFileOption}.\n' | |
| 'Invalid property line: $line', | |
| ); | |
| } | |
| final String key = keyValueMatch.group(1)!; | |
| final String value = keyValueMatch.group(2) ?? ''; | |
| // Remove wrapping quotes and trailing line comment. | |
| final Match? doubleQuotedValueMatch = DotEnvRegex.doubleQuotedValue.firstMatch(value); | |
| if (doubleQuotedValueMatch != null) { | |
| return MapEntry<String, String>(key, doubleQuotedValueMatch.group(1)!); | |
| } | |
| final Match? singleQuotedValueMatch = DotEnvRegex.singleQuotedValue.firstMatch(value); | |
| if (singleQuotedValueMatch != null) { | |
| return MapEntry<String, String>(key, singleQuotedValueMatch.group(1)!); | |
| } | |
| final Match? backQuotedValueMatch = DotEnvRegex.backQuotedValue.firstMatch(value); | |
| if (backQuotedValueMatch != null) { | |
| return MapEntry<String, String>(key, backQuotedValueMatch.group(1)!); | |
| } | |
| final Match? unquotedValueMatch = DotEnvRegex.unquotedValue.firstMatch(value); | |
| if (unquotedValueMatch != null) { | |
| return MapEntry<String, String>(key, unquotedValueMatch.group(1)!); | |
| } | |
| return MapEntry<String, String>(key, value); | |
| } | |
| /// Converts an .env file string to its equivalent JSON string. | |
| /// | |
| /// For example, the .env file string | |
| /// key=value # comment | |
| /// complexKey="foo#bar=baz" | |
| /// would be converted to a JSON string equivalent to: | |
| /// { | |
| /// "key": "value", | |
| /// "complexKey": "foo#bar=baz" | |
| /// } | |
| /// | |
| /// Multiline values are not supported. | |
| String convertEnvFileToJsonRaw(String configRaw) { | |
| final List<String> lines = configRaw | |
| .split('\n') | |
| .map((String line) => line.trim()) | |
| .where((String line) => line.isNotEmpty) | |
| .where((String line) => !line.startsWith('#')) // Remove comment lines. | |
| .toList(); | |
| final propertyMap = <String, String>{}; | |
| for (final line in lines) { | |
| final MapEntry<String, String> property = _parseProperty(line); | |
| propertyMap[property.key] = property.value; | |
| } | |
| return jsonEncode(propertyMap); | |
| } | |
| Map<String, String> extractWebHeaders() { | |
| final webHeaders = <String, String>{}; | |
| if (argParser.options.containsKey('web-header')) { | |
| final List<String> candidates = stringsArg('web-header'); | |
| final invalidHeaders = <String>[]; | |
| for (final candidate in candidates) { | |
| final Match? keyValueMatch = _HttpRegex.httpHeader.firstMatch(candidate); | |
| if (keyValueMatch == null) { | |
| invalidHeaders.add(candidate); | |
| continue; | |
| } | |
| webHeaders[keyValueMatch.group(1)!] = keyValueMatch.group(2)!; | |
| } | |
| if (invalidHeaders.isNotEmpty) { | |
| throwToolExit('Invalid web headers: ${invalidHeaders.join(', ')}'); | |
| } | |
| } | |
| return webHeaders; | |
| } | |
| void _registerSignalHandlers(String commandPath, DateTime startTime) { | |
| void handler(io.ProcessSignal s) { | |
| _cache.releaseLock(); | |
| _sendPostUsage( | |
| commandPath, | |
| const FlutterCommandResult(ExitStatus.killed), | |
| startTime, | |
| _clock.now(), | |
| ); | |
| } | |
| _signals.addHandler(ProcessSignal.sigterm, handler); | |
| _signals.addHandler(ProcessSignal.sigint, handler); | |
| } | |
| /// Logs data about this command. | |
| /// | |
| /// For example, the command path (e.g. `build/apk`) and the result, | |
| /// as well as the time spent running it. | |
| void _sendPostUsage( | |
| String commandPath, | |
| FlutterCommandResult commandResult, | |
| DateTime startTime, | |
| DateTime endTime, | |
| ) { | |
| // Send command result. | |
| final int? maxRss = getMaxRss(processInfo); | |
| _analytics.send( | |
| Event.flutterCommandResult( | |
| commandPath: commandPath, | |
| result: commandResult.toString(), | |
| maxRss: maxRss, | |
| commandHasTerminal: hasTerminal, | |
| ), | |
| ); | |
| // Send timing. | |
| final labels = <String?>[ | |
| commandResult.exitStatus.name, | |
| if (commandResult.timingLabelParts?.isNotEmpty ?? false) ...?commandResult.timingLabelParts, | |
| ]; | |
| final String label = labels | |
| .where((String? label) => label != null && !_isBlank(label)) | |
| .join('-'); | |
| // If the command provides its own end time, use it. Otherwise report | |
| // the duration of the entire execution. | |
| final Duration elapsedDuration = (commandResult.endTimeOverride ?? endTime).difference( | |
| startTime, | |
| ); | |
| _analytics.send( | |
| Event.timing( | |
| workflow: 'flutter', | |
| variableName: name, | |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, | |
| // Report in the form of `success-[parameter1-parameter2]`, all of which | |
| // can be null if the command doesn't provide a FlutterCommandResult. | |
| label: label == '' ? null : label, | |
| ), | |
| ); | |
| } | |
| /// Perform validation then call [runCommand] to execute the command. | |
| /// Return a [Future] that completes with an exit code | |
| /// indicating whether execution was successful. | |
| /// | |
| /// Subclasses should override this method to perform verification | |
| /// then call this method to execute the command | |
| /// rather than calling [runCommand] directly. | |
| @mustCallSuper | |
| Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async { | |
| _preRunValidator.validate(); | |
| if (argParser.options.containsKey(FlutterOptions.kEnableImpeller) && | |
| (argResults?.wasParsed(FlutterOptions.kEnableImpeller) ?? false)) { | |
| if (getBuildMode().isRelease) { | |
| final bool enableImpeller = boolArg(FlutterOptions.kEnableImpeller); | |
| final flagName = enableImpeller | |
| ? '--${FlutterOptions.kEnableImpeller}' | |
| : '--no-${FlutterOptions.kEnableImpeller}'; | |
| _logger.printWarning( | |
| 'The "$flagName" flag is ignored in release builds. ' | |
| 'The rendering backend is determined at build time.', | |
| ); | |
| } | |
| } | |
| final PersistentToolState? persistentToolState = _persistentToolState; | |
| if (_os.hostPlatform == HostPlatform.darwin_x64 && | |
| (persistentToolState?.shouldShowIntelMacWarning ?? true)) { | |
| _logger.printWarning( | |
| 'Flutter is deprecating support for Intel-based Macs. ' | |
| 'A future version of Flutter will require an Apple Silicon Mac to build applications.', | |
| ); | |
| persistentToolState?.shouldShowIntelMacWarning = false; | |
| } | |
| if (refreshWirelessDevices) { | |
| _targetDevices.startExtendedWirelessDeviceDiscovery( | |
| deviceDiscoveryTimeout: deviceDiscoveryTimeout, | |
| ); | |
| } | |
| final FlutterProject project; | |
| try { | |
| project = await _updateCacheAndRunPubGet(); | |
| } finally { | |
| _cache.releaseLock(); | |
| } | |
| if (regeneratePlatformSpecificToolingDuringVerify) { | |
| await regeneratePlatformSpecificToolingIfApplicable( | |
| project, | |
| releaseMode: getBuildMode().isRelease, | |
| ); | |
| } | |
| if (commandPath != null) { | |
| _analytics.send(await unifiedAnalyticsUsageValues(commandPath)); | |
| } | |
| return runCommand(); | |
| } | |
| Future<FlutterProject> _updateCacheAndRunPubGet() async { | |
| // Populate the cache. We call this before pub get below so that the | |
| // sky_engine package is available in the flutter cache for pub to find. | |
| if (shouldUpdateCache) { | |
| // First always update universal artifacts, as some of these (e.g. | |
| // ios-deploy on macOS) are required to determine `requiredArtifacts`. | |
| final bool offline; | |
| if (argParser.options.containsKey('offline')) { | |
| offline = boolArg('offline'); | |
| } else { | |
| offline = false; | |
| } | |
| await _cache.updateAll(<DevelopmentArtifact>{ | |
| DevelopmentArtifact.universal, | |
| }, offline: offline); | |
| await _cache.updateAll(await requiredArtifacts, offline: offline); | |
| } | |
| await validateCommand(); | |
| final FlutterProject project = _projectFactory.fromDirectory(_fs.currentDirectory); | |
| project.checkForDeprecation(deprecationBehavior: deprecationBehavior); | |
| if (shouldRunPub) { | |
| await pub.get( | |
| context: PubContext.getVerifyContext(name), | |
| project: project, | |
| checkUpToDate: cachePubGet, | |
| ); | |
| } | |
| return project; | |
| } | |
| /// Whether to run [FlutterProject.regeneratePlatformSpecificTooling] in [verifyThenRunCommand]. | |
| /// | |
| /// By default `true`, but sub-commands that do _meta_ builds (make multiple different | |
| /// builds sequentially in one-go) may choose to override this and provide `false`, instead | |
| /// calling [FlutterProject.regeneratePlatformSpecificTooling] manually when applicable. | |
| @visibleForOverriding | |
| bool get regeneratePlatformSpecificToolingDuringVerify => true; | |
| /// Runs [FlutterProject.regeneratePlatformSpecificTooling] for [project] with appropriate configuration. | |
| /// | |
| /// This method should only be called when [shouldRunPub] is `true`: | |
| /// ```dart | |
| /// if (shouldRunPub) { | |
| /// await regeneratePlatformSpecificTooling(project); | |
| /// } | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// - <https://github.com/flutter/flutter/issues/162649>. | |
| @protected | |
| @nonVirtual | |
| Future<void> regeneratePlatformSpecificToolingIfApplicable( | |
| FlutterProject project, { | |
| required bool releaseMode, | |
| }) async { | |
| if (!shouldRunPub) { | |
| return; | |
| } | |
| await project.regeneratePlatformSpecificTooling(releaseMode: releaseMode); | |
| } | |
| /// The set of development artifacts required for this command. | |
| /// | |
| /// Defaults to an empty set. Including [DevelopmentArtifact.universal] is | |
| /// not required as it is always updated. | |
| Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{}; | |
| /// Subclasses must implement this to execute the command. | |
| /// Optionally provide a [FlutterCommandResult] to send more details about the | |
| /// execution for analytics. | |
| Future<FlutterCommandResult> runCommand(); | |
| /// Find and return all target [Device]s based upon currently connected | |
| /// devices and criteria entered by the user on the command line. | |
| /// If no device can be found that meets specified criteria, | |
| /// then print an error message and return null. | |
| /// | |
| /// If [canPrompt] is true, the tool will interactively prompt the user to | |
| /// select a device when multiple devices are found and a terminal is | |
| /// attached. If [canPrompt] is false, the interactive prompt is bypassed. | |
| /// If not specified, [canPrompt] defaults to `!outputMachineFormat`. | |
| Future<List<Device>?> findAllTargetDevices({ | |
| bool? canPrompt, | |
| bool includeDevicesUnsupportedByProject = false, | |
| }) async { | |
| return _targetDevices.findAllTargetDevices( | |
| canPrompt: canPrompt ?? !outputMachineFormat, | |
| deviceDiscoveryTimeout: deviceDiscoveryTimeout, | |
| includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject, | |
| ); | |
| } | |
| /// Find and return the target [Device] based upon currently connected | |
| /// devices and criteria entered by the user on the command line. | |
| /// If a device cannot be found that meets specified criteria, | |
| /// then print an error message and return null. | |
| /// | |
| /// If [includeDevicesUnsupportedByProject] is true, the tool does not filter | |
| /// the list by the current project support list. | |
| Future<Device?> findTargetDevice({bool includeDevicesUnsupportedByProject = false}) async { | |
| final List<Device>? deviceList = await findAllTargetDevices( | |
| includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject, | |
| ); | |
| if (deviceList == null) { | |
| return null; | |
| } | |
| if (deviceList.length > 1) { | |
| _logger.printStatus(_userMessages.flutterSpecifyDevice); | |
| final List<Device> allDevices = await globals.deviceManager!.getAllDevices(); | |
| _logger.printStatus(''); | |
| await Device.printDevices(allDevices, _logger); | |
| return null; | |
| } | |
| return deviceList.single; | |
| } | |
| @protected | |
| @mustCallSuper | |
| Future<void> validateCommand() async { | |
| if (_requiresPubspecYaml && globalResults?.wasParsed('packages') != true) { | |
| // Don't expect a pubspec.yaml file if the user passed in an explicit package_config.json file path. | |
| // If there is no pubspec in the current directory, look in the parent | |
| // until one can be found. | |
| final String? path = findProjectRoot(_fs, _fs.currentDirectory.path); | |
| if (path == null) { | |
| throwToolExit(_userMessages.flutterNoPubspec); | |
| } | |
| if (path != _fs.currentDirectory.path) { | |
| _fs.currentDirectory = path; | |
| _logger.printStatus('Changing current working directory to: ${_fs.currentDirectory.path}'); | |
| } | |
| } | |
| if (_usesTargetOption) { | |
| final String targetPath = targetFile; | |
| if (!_fs.isFileSync(targetPath)) { | |
| throwToolExit(_userMessages.flutterTargetFileMissing(targetPath)); | |
| } | |
| } | |
| } | |
| @override | |
| String get usage { | |
| final String usageWithoutDescription = super.usage.substring( | |
| // The description plus two newlines. | |
| description.length + 2, | |
| ); | |
| final String help = <String>[ | |
| if (deprecated) | |
| '${_logger.terminal.warningMark} Deprecated. This command will be removed in a future version of Flutter.', | |
| description, | |
| '', | |
| 'Global options:', | |
| '${runner?.argParser.usage}', | |
| '', | |
| usageWithoutDescription, | |
| ].join('\n'); | |
| return help; | |
| } | |
| // TODO(bkonyi): Remove once commands using ApplicationPackageFactory are migrated to explicit dependency injection, https://github.com/flutter/flutter/issues/47161. | |
| ApplicationPackageFactory? get applicationPackages => | |
| _applicationPackages ?? ApplicationPackageFactory.instance; | |
| set applicationPackages(ApplicationPackageFactory? value) { | |
| _applicationPackages = value; | |
| } | |
| ApplicationPackageFactory? _applicationPackages; | |
| /// Gets the parsed command-line flag named [name] as a `bool`. | |
| /// | |
| /// If no flag named [name] was added to the [ArgParser], an [ArgumentError] | |
| /// will be thrown. | |
| bool boolArg(String name, {bool global = false}) { | |
| return (global ? globalResults : argResults)!.flag(name); | |
| } | |
| /// Gets the parsed command-line option named [name] as a `String`. | |
| /// | |
| /// If no option named [name] was added to the [ArgParser], an [ArgumentError] | |
| /// will be thrown. | |
| String? stringArg(String name, {bool global = false}) { | |
| return (global ? globalResults : argResults)!.option(name); | |
| } | |
| /// Gets the parsed command-line option named [name] as `List<String>`. | |
| List<String> stringsArg(String name, {bool global = false}) { | |
| return (global ? globalResults : argResults)!.multiOption(name); | |
| } | |
| } | |
| /// A mixin which applies an implementation of [requiredArtifacts] that only | |
| /// downloads artifacts corresponding to potentially connected devices. | |
| mixin DeviceBasedDevelopmentArtifacts on FlutterCommand { | |
| @override | |
| Future<Set<DevelopmentArtifact>> get requiredArtifacts async { | |
| // If there are no devices, use the default configuration. | |
| // Otherwise, only add development artifacts corresponding to | |
| // potentially connected devices. We might not be able to determine if a | |
| // device is connected yet, so include it in case it becomes connected. | |
| final List<Device> devices = await globals.deviceManager!.getDevices( | |
| filter: DeviceDiscoveryFilter(excludeDisconnected: false), | |
| ); | |
| if (devices.isEmpty) { | |
| return super.requiredArtifacts; | |
| } | |
| final artifacts = <DevelopmentArtifact>{DevelopmentArtifact.universal}; | |
| for (final device in devices) { | |
| final TargetPlatform targetPlatform = await device.targetPlatform; | |
| final DevelopmentArtifact? developmentArtifact = artifactFromTargetPlatform( | |
| targetPlatform, | |
| featureFlags, | |
| ); | |
| if (developmentArtifact != null) { | |
| artifacts.add(developmentArtifact); | |
| } | |
| } | |
| return artifacts; | |
| } | |
| } | |
| // Returns the development artifact for the target platform, or null | |
| // if none is supported | |
| @protected | |
| DevelopmentArtifact? artifactFromTargetPlatform( | |
| TargetPlatform targetPlatform, | |
| FeatureFlags featureFlags, | |
| ) { | |
| switch (targetPlatform) { | |
| case TargetPlatform.android: | |
| case TargetPlatform.android_arm: | |
| case TargetPlatform.android_arm64: | |
| case TargetPlatform.android_x64: | |
| return DevelopmentArtifact.androidGenSnapshot; | |
| case TargetPlatform.web_javascript: | |
| return DevelopmentArtifact.web; | |
| case TargetPlatform.fuchsia_arm64: | |
| case TargetPlatform.fuchsia_x64: | |
| return null; | |
| case TargetPlatform.ios: | |
| return DevelopmentArtifact.iOS; | |
| case TargetPlatform.darwin: | |
| if (featureFlags.isMacOSEnabled) { | |
| return DevelopmentArtifact.macOS; | |
| } | |
| return null; | |
| case TargetPlatform.windows_x64: | |
| case TargetPlatform.windows_arm64: | |
| if (featureFlags.isWindowsEnabled) { | |
| return DevelopmentArtifact.windows; | |
| } | |
| return null; | |
| case TargetPlatform.linux_x64: | |
| case TargetPlatform.linux_arm64: | |
| case TargetPlatform.linux_riscv64: | |
| if (featureFlags.isLinuxEnabled) { | |
| return DevelopmentArtifact.linux; | |
| } | |
| return null; | |
| case TargetPlatform.tester: | |
| case TargetPlatform.unsupported: | |
| return null; | |
| } | |
| } | |
| /// Returns true if s is either null, empty or is solely made of whitespace characters (as defined by String.trim). | |
| bool _isBlank(String s) => s.trim().isEmpty; |