flutter_command.dart 77.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:args/args.dart';
6
import 'package:args/command_runner.dart';
7
import 'package:file/file.dart';
8
import 'package:meta/meta.dart';
9
import 'package:package_config/package_config_types.dart';
10
import 'package:unified_analytics/unified_analytics.dart';
11 12

import '../application_package.dart';
13
import '../base/common.dart';
14
import '../base/context.dart';
15
import '../base/io.dart' as io;
16
import '../base/io.dart';
17
import '../base/os.dart';
18
import '../base/utils.dart';
19
import '../build_info.dart';
20
import '../build_system/build_system.dart';
21
import '../bundle.dart' as bundle;
22
import '../cache.dart';
23
import '../convert.dart';
24
import '../dart/generate_synthetic_packages.dart';
25
import '../dart/package_map.dart';
26
import '../dart/pub.dart';
27
import '../device.dart';
28
import '../features.dart';
29
import '../globals.dart' as globals;
30
import '../preview_device.dart';
31
import '../project.dart';
32
import '../reporting/reporting.dart';
33
import '../reporting/unified_analytics.dart';
34
import '../web/compile.dart';
35
import 'flutter_command_runner.dart';
36
import 'target_devices.dart';
37

38 39
export '../cache.dart' show DevelopmentArtifact;

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
abstract class DotEnvRegex {
  // Dot env multi-line block value regex
  static final RegExp 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 RegExp 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 RegExp 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 RegExp 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 RegExp 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 RegExp unquotedValue = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$');
}

65 66 67 68 69 70 71 72 73 74
abstract class _HttpRegex {
  // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
  static const String _vchar = r'\x21-\x7E';
  static const String _spaceOrTab = r'\x20\x09';
  static const String _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 RegExp httpHeader = RegExp('^([$_nonDelimiterVchar]+)' r'\s*=\s*' '([$_vchar$_spaceOrTab]+)' r'$');
}

75 76 77 78
enum ExitStatus {
  success,
  warning,
  fail,
79
  killed,
80 81 82
}

/// [FlutterCommand]s' subclasses' [FlutterCommand.runCommand] can optionally
83
/// provide a [FlutterCommandResult] to furnish additional information for
84 85
/// analytics.
class FlutterCommandResult {
86
  const FlutterCommandResult(
87
    this.exitStatus, {
88
    this.timingLabelParts,
89
    this.endTimeOverride,
90
  });
91

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
  /// 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);
  }

107 108
  final ExitStatus exitStatus;

109
  /// Optional data that can be appended to the timing event.
110 111
  /// https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#timingLabel
  /// Do not add PII.
112
  final List<String?>? timingLabelParts;
113

114
  /// Optional epoch time when the command's non-interactive wait time is
115
  /// complete during the command's execution. Use to measure user perceivable
116 117
  /// latency without measuring user interaction time.
  ///
118
  /// [FlutterCommand] will automatically measure and report the command's
119
  /// complete time if not overridden.
120
  final DateTime? endTimeOverride;
121 122

  @override
123
  String toString() => exitStatus.name;
124 125
}

126
/// Common flutter command line options.
127
abstract final class FlutterOptions {
128
  static const String kFrontendServerStarterPath = 'frontend-server-starter-path';
129 130
  static const String kExtraFrontEndOptions = 'extra-front-end-options';
  static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options';
131
  static const String kEnableExperiment = 'enable-experiment';
132 133
  static const String kFileSystemRoot = 'filesystem-root';
  static const String kFileSystemScheme = 'filesystem-scheme';
134
  static const String kSplitDebugInfoOption = 'split-debug-info';
135
  static const String kDartObfuscationOption = 'obfuscate';
136
  static const String kDartDefinesOption = 'dart-define';
137
  static const String kDartDefineFromFileOption = 'dart-define-from-file';
138
  static const String kBundleSkSLPathOption = 'bundle-sksl-path';
139
  static const String kPerformanceMeasurementFile = 'performance-measurement-file';
140
  static const String kNullSafety = 'sound-null-safety';
141
  static const String kDeviceUser = 'device-user';
142
  static const String kDeviceTimeout = 'device-timeout';
143
  static const String kDeviceConnection = 'device-connection';
144
  static const String kAnalyzeSize = 'analyze-size';
145
  static const String kCodeSizeDirectory = 'code-size-directory';
146
  static const String kNullAssertions = 'null-assertions';
147
  static const String kAndroidGradleDaemon = 'android-gradle-daemon';
148
  static const String kDeferredComponents = 'deferred-components';
149
  static const String kAndroidProjectArgs = 'android-project-arg';
150
  static const String kAndroidSkipBuildDependencyValidation = 'android-skip-build-dependency-validation';
151
  static const String kInitializeFromDill = 'initialize-from-dill';
152
  static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date';
153
  static const String kNativeAssetsYamlFile = 'native-assets-yaml-file';
154
  static const String kFatalWarnings = 'fatal-warnings';
155
  static const String kUseApplicationBinary = 'use-application-binary';
156
  static const String kWebBrowserFlag = 'web-browser-flag';
157
  static const String kWebRendererFlag = 'web-renderer';
158
  static const String kWebResourcesCdnFlag = 'web-resources-cdn';
159
  static const String kWebWasmFlag = 'wasm';
160 161
}

162
/// flutter command categories for usage.
163
abstract final class FlutterCommandCategory {
164 165 166 167 168
  static const String sdk = 'Flutter SDK';
  static const String project = 'Project';
  static const String tools = 'Tools & Devices';
}

169
abstract class FlutterCommand extends Command<void> {
170 171 172
  /// The currently executing command (or sub-command).
  ///
  /// Will be `null` until the top-most command has begun execution.
173
  static FlutterCommand? get current => context.get<FlutterCommand>();
174

175 176 177 178
  /// The option name for a custom VM Service port.
  static const String vmServicePortOption = 'vm-service-port';

  /// The option name for a custom VM Service port.
179
  static const String observatoryPortOption = 'observatory-port';
180

181 182 183
  /// The option name for a custom DevTools server address.
  static const String kDevToolsServerAddress = 'devtools-server-address';

184 185 186
  /// The flag name for whether to launch the DevTools or not.
  static const String kEnableDevTools = 'devtools';

187 188 189
  /// The flag name for whether or not to use ipv6.
  static const String ipv6Flag = 'ipv6';

190 191
  @override
  ArgParser get argParser => _argParser;
192
  final ArgParser _argParser = ArgParser(
193
    usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
194
  );
195

196
  @override
197
  FlutterCommandRunner? get runner => super.runner as FlutterCommandRunner?;
198

199 200
  bool _requiresPubspecYaml = false;

201 202 203 204
  /// Whether this command uses the 'target' option.
  bool _usesTargetOption = false;

  bool _usesPubOption = false;
205

206 207 208 209
  bool _usesPortOption = false;

  bool _usesIpv6Flag = false;

210 211
  bool _usesFatalWarnings = false;

212 213
  DeprecationBehavior get deprecationBehavior => DeprecationBehavior.none;

214
  bool get shouldRunPub => _usesPubOption && boolArg('pub');
215

216 217
  bool get shouldUpdateCache => true;

218 219
  bool get deprecated => false;

220 221
  ProcessInfo get processInfo => globals.processInfo;

222 223 224 225 226
  /// 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;

227 228 229
  @override
  bool get hidden => deprecated;

230
  bool _excludeDebug = false;
231
  bool _excludeRelease = false;
232

233 234 235 236 237
  /// 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 => globals.analytics;

238 239 240 241
  void requiresPubspecYaml() {
    _requiresPubspecYaml = true;
  }

242
  void usesWebOptions({ required bool verboseHelp }) {
243 244 245 246 247 248 249 250
    argParser.addMultiOption('web-header',
      help: 'Additional key-value pairs that will added by the web server '
            'as headers to all responses. Multiple headers can be passed by '
            'repeating "--web-header" multiple times.',
      valueHelp: 'X-Custom-Header=header-value',
      splitCommas: false,
      hide: !verboseHelp,
    );
251
    argParser.addOption('web-hostname',
252
      defaultsTo: 'localhost',
253 254 255 256 257
      help:
        'The hostname that the web sever will use to resolve an IP to serve '
        'from. The unresolved hostname is used to launch Chrome when using '
        'the chrome Device. The name "any" may also be used to serve on any '
        'IPV4 for either the Chrome or web-server device.',
258
      hide: !verboseHelp,
259
    );
260
    argParser.addOption('web-port',
261 262
      help: 'The host port to serve the web application from. If not provided, the tool '
        'will select a random open port on the host.',
263
      hide: !verboseHelp,
264
    );
265 266 267 268 269 270 271 272 273 274
    argParser.addOption(
      'web-tls-cert-path',
      help: 'The certificate that host will use to serve using TLS connection. '
          'If not provided, the tool will use default http scheme.',
    );
    argParser.addOption(
      'web-tls-cert-key-path',
      help: 'The certificate key that host will use to authenticate cert. '
          'If not provided, the tool will use default http scheme.',
    );
275 276
    argParser.addOption('web-server-debug-protocol',
      allowed: <String>['sse', 'ws'],
277
      defaultsTo: 'ws',
278
      help: 'The protocol (SSE or WebSockets) to use for the debug service proxy '
279
      'when using the Web Server device and Dart Debug extension. '
280 281
      'This is useful for editors/debug adapters that do not support debugging '
      'over SSE (the default protocol for Web Server/Dart Debugger extension).',
282
      hide: !verboseHelp,
283
    );
284 285
    argParser.addOption('web-server-debug-backend-protocol',
      allowed: <String>['sse', 'ws'],
286
      defaultsTo: 'ws',
287 288 289 290
      help: 'The protocol (SSE or WebSockets) to use for the Dart Debug Extension '
      'backend service when using the Web Server device. '
      'Using WebSockets can improve performance but may fail when connecting through '
      'some proxy servers.',
291
      hide: !verboseHelp,
292
    );
293 294
    argParser.addOption('web-server-debug-injected-client-protocol',
      allowed: <String>['sse', 'ws'],
295
      defaultsTo: 'ws',
296 297 298 299 300 301
      help: 'The protocol (SSE or WebSockets) to use for the injected client '
      'when using the Web Server device. '
      'Using WebSockets can improve performance but may fail when connecting through '
      'some proxy servers.',
      hide: !verboseHelp,
    );
302 303 304
    argParser.addFlag('web-allow-expose-url',
      help: 'Enables daemon-to-editor requests (app.exposeUrl) for exposing URLs '
        'when running on remote machines.',
305
      hide: !verboseHelp,
306
    );
307 308 309
    argParser.addFlag('web-run-headless',
      help: 'Launches the browser in headless mode. Currently only Chrome '
        'supports this option.',
310
      hide: !verboseHelp,
311 312 313 314 315 316
    );
    argParser.addOption('web-browser-debug-port',
      help: 'The debug port the browser should use. If not specified, a '
        'random port is selected. Currently only Chrome supports this option. '
        'It serves the Chrome DevTools Protocol '
        '(https://chromedevtools.github.io/devtools-protocol/).',
317
      hide: !verboseHelp,
318
    );
319
    argParser.addFlag('web-enable-expression-evaluation',
320
      defaultsTo: true,
321
      help: 'Enables expression evaluation in the debugger.',
322
      hide: !verboseHelp,
323
    );
324 325 326 327
    argParser.addOption('web-launch-url',
      help: 'The URL to provide to the browser. Defaults to an HTTP URL with the host '
          'name of "--web-hostname", the port of "--web-port", and the path set to "/".',
    );
328 329 330 331 332 333 334 335 336
    argParser.addMultiOption(
      FlutterOptions.kWebBrowserFlag,
      help: 'Additional flag to pass to a browser instance at startup.\n'
          'Chrome: https://www.chromium.org/developers/how-tos/run-chromium-with-flags/\n'
          'Firefox: https://wiki.mozilla.org/Firefox/CommandLineOptions\n'
          'Multiple flags can be passed by repeating "--${FlutterOptions.kWebBrowserFlag}" multiple times.',
      valueHelp: '--foo=bar',
      hide: !verboseHelp,
    );
337 338
  }

339 340 341
  void usesTargetOption() {
    argParser.addOption('target',
      abbr: 't',
342
      defaultsTo: bundle.defaultMainPath,
343
      help: 'The main entry-point file of the application, as run on the device.\n'
344
            'If the "--target" option is omitted, but a file name is provided on '
345 346
            'the command line, then that is used instead.',
      valueHelp: 'path');
347 348 349
    _usesTargetOption = true;
  }

350 351 352 353 354 355 356 357 358
  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;
  }

359
  String get targetFile {
360
    if (argResults?.wasParsed('target') ?? false) {
361
      return stringArg('target')!;
362
    }
363 364 365
    final List<String>? rest = argResults?.rest;
    if (rest != null && rest.isNotEmpty) {
      return rest.first;
366 367
    }
    return bundle.defaultMainPath;
368 369
  }

370
  /// Indicates if the current command running has a terminal attached.
371 372
  bool get hasTerminal => globals.stdio.hasTerminal;

373 374 375
  /// Path to the Dart's package config file.
  ///
  /// This can be overridden by some of its subclasses.
376 377 378 379
  String? get packagesPath => stringArg(FlutterGlobalOptions.kPackagesOption, global: true);

  /// Whether flutter is being run from our CI.
  bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true);
380

381 382
  String? get debugLogsDirectoryPath => stringArg(FlutterGlobalOptions.kDebugLogsDirectoryFlag, global: true);

383 384 385
  /// The value of the `--filesystem-scheme` argument.
  ///
  /// This can be overridden by some of its subclasses.
386
  String? get fileSystemScheme =>
387
    argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
388
          ? stringArg(FlutterOptions.kFileSystemScheme)
389 390 391 392 393
          : null;

  /// The values of the `--filesystem-root` argument.
  ///
  /// This can be overridden by some of its subclasses.
394
  List<String>? get fileSystemRoots =>
395 396 397 398
    argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
          ? stringsArg(FlutterOptions.kFileSystemRoot)
          : null;

399
  void usesPubOption({bool hide = false}) {
400 401
    argParser.addFlag('pub',
      defaultsTo: true,
402
      hide: hide,
403
      help: 'Whether to run "flutter pub get" before executing this command.');
404 405 406
    _usesPubOption = true;
  }

407 408
  /// Adds flags for using a specific filesystem root and scheme.
  ///
409 410
  /// The `hide` argument indicates whether or not to hide these options when
  /// the user asks for help.
411
  void usesFilesystemOptions({ required bool hide }) {
412 413 414 415 416 417 418
    argParser
      ..addOption('output-dill',
        hide: hide,
        help: 'Specify the path to frontend server output kernel file.',
      )
      ..addMultiOption(FlutterOptions.kFileSystemRoot,
        hide: hide,
419 420 421 422
        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.',
423 424 425 426
      )
      ..addOption(FlutterOptions.kFileSystemScheme,
        defaultsTo: 'org-dartlang-root',
        hide: hide,
427 428
        help: 'Specify the scheme that is used for virtual file system used in '
              'compilation. See also the "--${FlutterOptions.kFileSystemRoot}" option.',
429 430 431
      );
  }

432
  /// Adds options for connecting to the Dart VM Service port.
433
  void usesPortOptions({ required bool verboseHelp }) {
434 435 436 437 438 439 440 441
    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,
    );
442
    argParser.addOption(observatoryPortOption,
443
        help: '(deprecated; use host-vmservice-port instead) '
444
              'Listen to the given port for a Dart VM Service connection.\n'
445
              'Specifying port 0 (the default) will find a random free port.\n '
446
              'if the Dart Development Service (DDS) is enabled, this will not be the port '
447
              'of the VmService instance advertised on the command line.',
448
        hide: !verboseHelp,
449
    );
450 451 452 453 454 455 456 457 458 459
    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.'
    );
460 461 462
    _usesPortOption = true;
  }

463 464 465 466 467 468 469 470 471 472 473 474 475
  /// Add option values for output directory of artifacts
  void usesOutputDir() {
    // TODO(eliasyishak): this feature has been added to [BuildWebCommand] and
    //  [BuildAarCommand]
    argParser.addOption('output',
        abbr: 'o',
        aliases: <String>['output-dir'],
        help:
            'The absolute path to the directory where the repository is generated. '
            'By default, this is <current-directory>/build/<target-platform>.\n'
            'Currently supported for subcommands: aar, web.');
  }

476
  void addDevToolsOptions({required bool verboseHelp}) {
477 478 479 480
    argParser.addFlag(
      kEnableDevTools,
      hide: !verboseHelp,
      defaultsTo: true,
481
      help: 'Enable (or disable, with "--no-$kEnableDevTools") the launching of the '
482
            'Flutter DevTools debugger and profiler. '
483
            'If specified, "--$kDevToolsServerAddress" is ignored.'
484 485 486 487
    );
    argParser.addOption(
      kDevToolsServerAddress,
      hide: !verboseHelp,
488
      help: 'When this value is provided, the Flutter tool will not spin up a '
489
            'new DevTools server instance, and will instead use the one provided '
490
            'at the given address. Ignored if "--no-$kEnableDevTools" is specified.'
491
    );
492 493
  }

494
  void addDdsOptions({required bool verboseHelp}) {
495 496
    argParser.addOption('dds-port',
      help: 'When this value is provided, the Dart Development Service (DDS) will be '
497 498 499
            'bound to the provided port.\n'
            'Specifying port 0 (the default) will find a random free port.'
    );
500
    argParser.addFlag(
501
      'dds',
502
      hide: !verboseHelp,
503 504 505 506 507 508 509 510 511 512 513 514 515
      defaultsTo: true,
      help: 'Enable the Dart Developer Service (DDS).\n'
            'It may be necessary to disable this when attaching to an application with '
            'an existing DDS instance (e.g., attaching to an application currently '
            'connected to by "flutter run"), or when running certain tests.\n'
            'Disabling this feature may degrade IDE functionality if a DDS instance is '
            'not already connected to the target application.'
    );
    argParser.addFlag(
      'disable-dds',
      hide: !verboseHelp,
      help: '(deprecated; use "--no-dds" instead) '
            'Disable the Dart Developer Service (DDS).'
516 517 518
    );
  }

519 520 521 522 523 524 525
  void addServeObservatoryOptions({required bool verboseHelp}) {
    argParser.addFlag('serve-observatory',
      hide: !verboseHelp,
      help: 'Serve the legacy Observatory developer tooling through the VM service.',
    );
  }

526 527
  late final bool enableDds = () {
    bool ddsEnabled = false;
528 529
    if (argResults?.wasParsed('disable-dds') ?? false) {
      if (argResults?.wasParsed('dds') ?? false) {
530 531 532
        throwToolExit(
            'The "--[no-]dds" and "--[no-]disable-dds" arguments are mutually exclusive. Only specify "--[no-]dds".');
      }
533
      ddsEnabled = !boolArg('disable-dds');
534
      // TODO(ianh): enable the following code once google3 is migrated away from --disable-dds (and add test to flutter_command_test.dart)
535
      if (false) { // ignore: dead_code, literal_only_boolean_expressions
536
        if (ddsEnabled) {
537
          globals.printWarning('${globals.logger.terminal
538 539
              .warningMark} The "--no-disable-dds" argument is deprecated and redundant, and should be omitted.');
        } else {
540
          globals.printWarning('${globals.logger.terminal
541
              .warningMark} The "--disable-dds" argument is deprecated. Use "--no-dds" instead.');
542 543
        }
      }
544
    } else {
545
      ddsEnabled = boolArg('dds');
546
    }
547 548
    return ddsEnabled;
  }();
549

550 551
  bool get _hostVmServicePortProvided => (argResults?.wasParsed(vmServicePortOption) ?? false)
      || (argResults?.wasParsed(observatoryPortOption) ?? false)
552
      || (argResults?.wasParsed('host-vmservice-port') ?? false);
553 554

  int _tryParseHostVmservicePort() {
555 556 557
    final String? vmServicePort = stringArg(vmServicePortOption) ??
                                  stringArg(observatoryPortOption);
    final String? hostPort = stringArg('host-vmservice-port');
558 559
    if (vmServicePort == null && hostPort == null) {
      throwToolExit('Invalid port for `--vm-service-port/--host-vmservice-port`');
560
    }
561
    try {
562
      return int.parse((vmServicePort ?? hostPort)!);
563
    } on FormatException catch (error) {
564
      throwToolExit('Invalid port for `--vm-service-port/--host-vmservice-port`: $error');
565 566 567
    }
  }

568
  int get ddsPort {
569
    if (argResults?.wasParsed('dds-port') != true && _hostVmServicePortProvided) {
570 571
      // If an explicit DDS port is _not_ provided, use the host-vmservice-port for DDS.
      return _tryParseHostVmservicePort();
572
    } else if (argResults?.wasParsed('dds-port') ?? false) {
573
      // If an explicit DDS port is provided, use dds-port for DDS.
574
      return int.tryParse(stringArg('dds-port')!) ?? 0;
575
    }
576
    // Otherwise, DDS can bind to a random port.
577 578 579
    return 0;
  }

580
  Uri? get devToolsServerAddress {
581
    if (argResults?.wasParsed(kDevToolsServerAddress) ?? false) {
582
      final Uri? uri = Uri.tryParse(stringArg(kDevToolsServerAddress)!);
583 584 585 586 587 588 589
      if (uri != null && uri.host.isNotEmpty && uri.port != 0) {
        return uri;
      }
    }
    return null;
  }

590
  /// Gets the vmservice port provided to in the 'vm-service-port' or
591 592
  /// 'host-vmservice-port option.
  ///
593
  /// Only one of "host-vmservice-port" and "vm-service-port" may be
594 595 596
  /// specified.
  ///
  /// If no port is set, returns null.
597
  int? get hostVmservicePort {
598
    if (!_usesPortOption || !_hostVmServicePortProvided) {
599 600
      return null;
    }
601 602
    if ((argResults?.wasParsed(vmServicePortOption) ?? false)
        && (argResults?.wasParsed(observatoryPortOption) ?? false)
603
        && (argResults?.wasParsed('host-vmservice-port') ?? false)) {
604
      throwToolExit('Only one of "--vm-service-port" and '
605 606
        '"--host-vmservice-port" may be specified.');
    }
607 608 609
    // 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.
610
    if (enableDds && argResults?.wasParsed('dds-port') != true) {
611
      return null;
612
    }
613
    return _tryParseHostVmservicePort();
614 615 616
  }

  /// Gets the vmservice port provided to in the 'device-vmservice-port' option.
617 618
  ///
  /// If no port is set, returns null.
619
  int? get deviceVmservicePort {
620
    final String? devicePort = stringArg('device-vmservice-port');
621
    if (!_usesPortOption || devicePort == null) {
622 623 624
      return null;
    }
    try {
625
      return int.parse(devicePort);
626 627
    } on FormatException catch (error) {
      throwToolExit('Invalid port for `--device-vmservice-port`: $error');
628 629 630
    }
  }

631 632
  void addPublishPort({ bool enabledByDefault = true, bool verboseHelp = false }) {
    argParser.addFlag('publish-port',
633 634 635 636 637
      hide: !verboseHelp,
      help: 'Publish the VM service port over mDNS. Disable to prevent the '
            'local network permission app dialog in debug and profile build modes (iOS devices only).',
      defaultsTo: enabledByDefault,
    );
638 639
  }

640
  Future<bool> get disablePortPublication async => !boolArg('publish-port');
641

642
  void usesIpv6Flag({required bool verboseHelp}) {
643 644 645
    argParser.addFlag(ipv6Flag,
      negatable: false,
      help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool '
646
            'forwards the host port to a device port.',
647
      hide: !verboseHelp,
648 649 650 651
    );
    _usesIpv6Flag = true;
  }

652
  bool? get ipv6 => _usesIpv6Flag ? boolArg('ipv6') : null;
653

654 655
  void usesBuildNumberOption() {
    argParser.addOption('build-number',
656 657
        help: 'An identifier used as an internal version number.\n'
              'Each build must have a unique identifier to differentiate it from previous builds.\n'
658
              'It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.\n'
659
              'On Android it is used as "versionCode".\n'
660 661
              'On Xcode builds it is used as "CFBundleVersion".\n'
              'On Windows it is used as the build suffix for the product and file versions.',
662
    );
663 664 665 666 667 668
  }

  void usesBuildNameOption() {
    argParser.addOption('build-name',
        help: 'A "x.y.z" string used as the version number shown to users.\n'
              'For each new version of your app, you will provide a version number to differentiate it from previous versions.\n'
669
              'On Android it is used as "versionName".\n'
670 671
              'On Xcode builds it is used as "CFBundleShortVersionString".\n'
              'On Windows it is used as the major, minor, and patch parts of the product and file versions.',
672 673 674
        valueHelp: 'x.y.z');
  }

675
  void usesDartDefineOption() {
676
    argParser.addMultiOption(
677 678
      FlutterOptions.kDartDefinesOption,
      aliases: <String>[ kDartDefines ], // supported for historical reasons
679
      help: 'Additional key-value pairs that will be available as constants '
680 681
            'from the String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment '
            'constructors.\n'
682
            'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefinesOption}" multiple times.',
683
      valueHelp: 'foo=bar',
684
      splitCommas: false,
685
    );
686
    _usesDartDefineFromFileOption();
687 688
  }

689
  void _usesDartDefineFromFileOption() {
690
    argParser.addMultiOption(
691
      FlutterOptions.kDartDefineFromFileOption,
692 693 694
      help:
          'The path of a .json or .env file containing key-value pairs that will be available as environment variables.\n'
          'These can be accessed using the String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment constructors.\n'
695 696
          'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.\n'
          'Entries from "--${FlutterOptions.kDartDefinesOption}" with identical keys take precedence over entries from these files.',
697
      valueHelp: 'use-define-config.json|.env',
698
      splitCommas: false,
699
    );
700 701
  }

702
  void usesWebRendererOption() {
703 704
    argParser.addOption(
      FlutterOptions.kWebRendererFlag,
705 706
      defaultsTo: WebRendererMode.auto.name,
      allowed: WebRendererMode.values.map((WebRendererMode e) => e.name),
707
      help: 'The renderer implementation to use when building for the web.',
708
      allowedHelp: CliEnum.allowedHelp(WebRendererMode.values)
709 710 711
    );
  }

712 713 714
  void usesWebResourcesCdnFlag() {
    argParser.addFlag(
      FlutterOptions.kWebResourcesCdnFlag,
715
      defaultsTo: true,
716 717 718 719
      help: 'Use Web static resources hosted on a CDN.',
    );
  }

720 721 722 723 724 725
  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');
  }

726 727 728 729 730 731 732 733
  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'
    );
  }

734 735 736 737 738 739 740 741 742 743 744 745 746
  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.'
      },
    );
  }

747 748 749 750 751 752 753 754 755 756
  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',
    );
  }

757 758 759
  /// Whether it is safe for this command to use a cached pub invocation.
  bool get cachePubGet => true;

760 761 762
  /// Whether this command should report null safety analytics.
  bool get reportNullSafety => false;

763
  late final Duration? deviceDiscoveryTimeout = () {
764 765
    if ((argResults?.options.contains(FlutterOptions.kDeviceTimeout) ?? false)
        && (argResults?.wasParsed(FlutterOptions.kDeviceTimeout) ?? false)) {
766
      final int? timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout)!);
767
      if (timeoutSeconds == null) {
768
        throwToolExit( 'Could not parse "--${FlutterOptions.kDeviceTimeout}" argument. It must be an integer.');
769
      }
770
      return Duration(seconds: timeoutSeconds);
771
    }
772 773
    return null;
  }();
774

775 776 777 778 779 780 781 782 783 784 785 786 787
  DeviceConnectionInterface? get deviceConnectionInterface  {
    if ((argResults?.options.contains(FlutterOptions.kDeviceConnection) ?? false)
        && (argResults?.wasParsed(FlutterOptions.kDeviceConnection) ?? false)) {
      final String? connectionType = stringArg(FlutterOptions.kDeviceConnection);
      if (connectionType == 'attached') {
        return DeviceConnectionInterface.attached;
      } else if (connectionType == 'wireless') {
        return DeviceConnectionInterface.wireless;
      }
    }
    return null;
  }

788
  late final TargetDevices _targetDevices = TargetDevices(
789
    platform: globals.platform,
790 791
    deviceManager: globals.deviceManager!,
    logger: globals.logger,
792
    deviceConnectionInterface: deviceConnectionInterface,
793 794
  );

795
  void addBuildModeFlags({
796
    required bool verboseHelp,
797 798 799 800
    bool defaultToRelease = true,
    bool excludeDebug = false,
    bool excludeRelease = false,
  }) {
801 802 803
    // A release build must be the default if a debug build is not possible.
    assert(defaultToRelease || !excludeDebug);
    _excludeDebug = excludeDebug;
804
    _excludeRelease = excludeRelease;
805
    defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug;
806

807 808 809 810 811
    if (!excludeDebug) {
      argParser.addFlag('debug',
        negatable: false,
        help: 'Build a debug version of your app${defaultToRelease ? '' : ' (default mode)'}.');
    }
812 813
    argParser.addFlag('profile',
      negatable: false,
814
      help: 'Build a version of your app specialized for performance profiling.');
815 816 817 818 819 820 821 822 823
    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)' : ''}.');
    }
824 825
  }

826 827 828
  void addSplitDebugInfoOption() {
    argParser.addOption(FlutterOptions.kSplitDebugInfoOption,
      help: 'In a release build, this flag reduces application size by storing '
829 830 831 832 833 834 835
            'Dart program symbols in a separate file on the host rather than in the '
            'application. The value of the flag should be a directory where program '
            'symbol files can be stored for later use. These symbol files contain '
            'the information needed to symbolize Dart stack traces. For an app built '
            'with this flag, the "flutter symbolize" command with the right program '
            'symbol file is required to obtain a human readable stack trace.\n'
            'This flag cannot be combined with "--${FlutterOptions.kAnalyzeSize}".',
836
      valueHelp: 'v1.2.3/',
837 838 839
    );
  }

840 841 842
  void addDartObfuscationOption() {
    argParser.addFlag(FlutterOptions.kDartObfuscationOption,
      help: 'In a release build, this flag removes identifiers and replaces them '
843 844 845 846 847 848 849 850 851 852 853 854
            'with randomized values for the purposes of source code obfuscation. This '
            'flag must always be combined with "--${FlutterOptions.kSplitDebugInfoOption}" option, the '
            'mapping between the values and the original identifiers is stored in the '
            'symbol map created in the specified directory. For an app built with this '
            'flag, the "flutter symbolize" command with the right program '
            'symbol file is required to obtain a human readable stack trace.\n'
            '\n'
            'Because all identifiers are renamed, methods like Object.runtimeType, '
            'Type.toString, Enum.toString, Stacktrace.toString, Symbol.toString '
            '(for constant symbols or those generated by runtime system) will '
            'return obfuscated results. Any code or tests that rely on exact names '
            'will break.'
855 856 857
    );
  }

858
  void addBundleSkSLPathOption({ required bool hide }) {
859 860 861 862 863
    argParser.addOption(FlutterOptions.kBundleSkSLPathOption,
      help: 'A path to a file containing precompiled SkSL shaders generated '
        'during "flutter run". These can be included in an application to '
        'improve the first frame render times.',
      hide: hide,
864
      valueHelp: 'flutter_1.sksl'
865 866 867
    );
  }

868
  void addTreeShakeIconsFlag({
869
    bool? enabledByDefault
870
  }) {
871
    argParser.addFlag('tree-shake-icons',
872 873
      defaultsTo: enabledByDefault
        ?? kIconTreeShakerEnabledDefault,
874 875 876 877
      help: 'Tree shake icon fonts so that only glyphs used by the application remain.',
    );
  }

878
  void addShrinkingFlag({ required bool verboseHelp }) {
Emmanuel Garcia's avatar
Emmanuel Garcia committed
879
    argParser.addFlag('shrink',
880 881
      hide: !verboseHelp,
      help: 'This flag has no effect. Code shrinking is always enabled in release builds. '
882 883
            'To learn more, see: https://developer.android.com/studio/build/shrink-code'
    );
Emmanuel Garcia's avatar
Emmanuel Garcia committed
884 885
  }

886 887
  void addNullSafetyModeOptions({ required bool hide }) {
    argParser.addFlag(FlutterOptions.kNullSafety,
888
      help: 'This flag is deprecated as only null-safe code is supported.',
889
      defaultsTo: true,
890
      hide: true,
891 892
    );
    argParser.addFlag(FlutterOptions.kNullAssertions,
893 894
      help: 'This flag is deprecated as only null-safe code is supported.',
      hide: true,
895 896 897
    );
  }

898 899 900 901 902 903 904 905 906 907 908 909
  void usesFrontendServerStarterPathOption({required bool verboseHelp}) {
    argParser.addOption(
      FlutterOptions.kFrontendServerStarterPath,
      help: 'When this value is provided, the frontend server will be started '
            'in JIT mode from the specified file, instead of from the AOT '
            'snapshot shipped with the Dart SDK. The specified file can either '
            'be a Dart source file, or an AppJIT snapshot. This option does '
            'not affect web builds.',
      hide: !verboseHelp,
    );
  }

910 911
  /// Enables support for the hidden options --extra-front-end-options and
  /// --extra-gen-snapshot-options.
912
  void usesExtraDartFlagOptions({ required bool verboseHelp }) {
913 914
    argParser.addMultiOption(FlutterOptions.kExtraFrontEndOptions,
      aliases: <String>[ kExtraFrontEndOptions ], // supported for historical reasons
915 916 917 918
      help: 'A comma-separated list of additional command line arguments that will be passed directly to the Dart front end. '
            'For example, "--${FlutterOptions.kExtraFrontEndOptions}=--enable-experiment=nonfunction-type-aliases".',
      valueHelp: '--foo,--bar',
      hide: !verboseHelp,
919
    );
920 921
    argParser.addMultiOption(FlutterOptions.kExtraGenSnapshotOptions,
      aliases: <String>[ kExtraGenSnapshotOptions ], // supported for historical reasons
922
      help: 'A comma-separated list of additional command line arguments that will be passed directly to the Dart native compiler. '
923
            '(Only used in "--profile" or "--release" builds.) '
924 925 926
            'For example, "--${FlutterOptions.kExtraGenSnapshotOptions}=--no-strip".',
      valueHelp: '--foo,--bar',
      hide: !verboseHelp,
927
    );
928 929
  }

930
  void usesFuchsiaOptions({ bool hide = false }) {
931 932
    argParser.addOption(
      'target-model',
933
      help: 'Target model that determines what core libraries are available.',
934 935 936 937 938 939 940 941
      defaultsTo: 'flutter',
      hide: hide,
      allowed: const <String>['flutter', 'flutter_runner'],
    );
    argParser.addOption(
      'module',
      abbr: 'm',
      hide: hide,
942
      help: 'The name of the module (required if attaching to a fuchsia device).',
943 944 945 946
      valueHelp: 'module-name',
    );
  }

947
  void addEnableExperimentation({ required bool hide }) {
948 949 950
    argParser.addMultiOption(
      FlutterOptions.kEnableExperiment,
      help:
951
        'The name of an experimental Dart feature to enable. For more information see: '
952
        'https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md',
953
      hide: hide,
954 955 956
    );
  }

957 958 959 960 961
  void addBuildPerformanceFile({ bool hide = false }) {
    argParser.addOption(
      FlutterOptions.kPerformanceMeasurementFile,
      help:
        'The name of a file where flutter assemble performance and '
962 963
        'cached-ness information will be written in a JSON format.',
      hide: hide,
964 965 966
    );
  }

967 968 969 970
  void addAndroidSpecificBuildOptions({ bool hide = false }) {
    argParser.addFlag(
      FlutterOptions.kAndroidGradleDaemon,
      help: 'Whether to enable the Gradle daemon when performing an Android build. '
971 972 973 974
            'Starting the daemon is the default behavior of the gradle wrapper script created '
            'in a Flutter project. Setting this flag to false corresponds to passing '
            '"--no-daemon" to the gradle wrapper script. This flag will cause the daemon '
            'process to terminate after the build is completed.',
975
      defaultsTo: true,
976
      hide: hide,
977
    );
978 979 980 981 982 983
    argParser.addFlag(
      FlutterOptions.kAndroidSkipBuildDependencyValidation,
      help: 'Whether to skip version checking for Java, Gradle, '
          'the Android Gradle Plugin (AGP), and the Kotlin Gradle Plugin (KGP)'
          ' during Android builds.',
    );
984 985 986
    argParser.addMultiOption(
      FlutterOptions.kAndroidProjectArgs,
      help: 'Additional arguments specified as key=value that are passed directly to the gradle '
987
            'project via the -P flag. These can be accessed in build.gradle via the "project.property" API.',
988 989 990
      splitCommas: false,
      abbr: 'P',
    );
991 992
  }

993 994 995 996 997 998 999 1000
  void addNativeNullAssertions({ bool hide = false }) {
    argParser.addFlag('native-null-assertions',
      defaultsTo: true,
      hide: hide,
      help: 'Enables additional runtime null checks in web applications to ensure '
        'the correct nullability of native (such as in dart:html) and external '
        '(such as with JS interop) types. This is enabled by default but only takes '
        'effect in sound mode. To report an issue with a null assertion failure in '
1001 1002
        'dart:html or the other dart web libraries, please file a bug at: '
        'https://github.com/dart-lang/sdk/issues/labels/web-libraries'
1003 1004 1005
    );
  }

1006
  void usesInitializeFromDillOption({ required bool hide }) {
1007 1008 1009 1010 1011
    argParser.addOption(FlutterOptions.kInitializeFromDill,
      help: 'Initializes the resident compiler with a specific kernel file instead of '
        'the default cached location.',
      hide: hide,
    );
1012 1013 1014 1015 1016
    argParser.addFlag(FlutterOptions.kAssumeInitializeFromDillUpToDate,
      help: 'If set, assumes that the file passed in initialize-from-dill is up '
        'to date and skip the check and potential invalidation of files.',
      hide: hide,
    );
1017 1018
  }

1019 1020 1021 1022 1023 1024 1025 1026
  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,
    );
  }

1027 1028 1029 1030 1031 1032 1033 1034 1035
  void addIgnoreDeprecationOption({ bool hide = false }) {
    argParser.addFlag('ignore-deprecation',
      negatable: false,
      help: 'Indicates that the app should ignore deprecation warnings and continue to build '
            'using deprecated APIs. Use of this flag may cause your app to fail to build when '
            'deprecated APIs are removed.',
    );
  }

1036
  /// Adds build options common to all of the desktop build commands.
1037
  void addCommonDesktopBuildOptions({ required bool verboseHelp }) {
1038 1039 1040 1041 1042
    addBuildModeFlags(verboseHelp: verboseHelp);
    addBuildPerformanceFile(hide: !verboseHelp);
    addBundleSkSLPathOption(hide: !verboseHelp);
    addDartObfuscationOption();
    addEnableExperimentation(hide: !verboseHelp);
1043
    addNullSafetyModeOptions(hide: !verboseHelp);
1044 1045 1046 1047
    addSplitDebugInfoOption();
    addTreeShakeIconsFlag();
    usesAnalyzeSizeFlag();
    usesDartDefineOption();
1048
    usesExtraDartFlagOptions(verboseHelp: verboseHelp);
1049 1050 1051
    usesPubOption();
    usesTargetOption();
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
1052 1053
    usesBuildNumberOption();
    usesBuildNameOption();
1054 1055
  }

1056 1057 1058 1059
  /// The build mode that this command will use if no build mode is
  /// explicitly specified.
  ///
  /// Use [getBuildMode] to obtain the actual effective build mode.
1060
  BuildMode defaultBuildMode = BuildMode.debug;
1061

1062
  BuildMode getBuildMode() {
1063 1064
    // No debug when _excludeDebug is true.
    // If debug is not excluded, then take the command line flag.
1065 1066 1067
    final bool debugResult = !_excludeDebug && boolArg('debug');
    final bool jitReleaseResult = !_excludeRelease && boolArg('jit-release');
    final bool releaseResult = !_excludeRelease && boolArg('release');
1068 1069
    final List<bool> modeFlags = <bool>[
      debugResult,
1070
      jitReleaseResult,
1071
      boolArg('profile'),
1072
      releaseResult,
1073
    ];
1074
    if (modeFlags.where((bool flag) => flag).length > 1) {
1075
      throw UsageException('Only one of "--debug", "--profile", "--jit-release", '
1076
                           'or "--release" can be specified.', '');
1077
    }
1078
    if (debugResult) {
1079
      return BuildMode.debug;
1080
    }
1081
    if (boolArg('profile')) {
1082 1083
      return BuildMode.profile;
    }
1084
    if (releaseResult) {
1085 1086
      return BuildMode.release;
    }
1087
    if (jitReleaseResult) {
1088 1089
      return BuildMode.jitRelease;
    }
1090
    return defaultBuildMode;
1091 1092
  }

1093 1094 1095 1096
  void usesFlavorOption() {
    argParser.addOption(
      'flavor',
      help: 'Build a custom app flavor as defined by platform-specific build setup.\n'
1097 1098 1099 1100 1101
            'Supports the use of product flavors in Android Gradle scripts, and '
            'the use of custom Xcode schemes.',
    );
  }

1102
  void usesTrackWidgetCreation({ bool hasEffect = true, required bool verboseHelp }) {
1103 1104 1105
    argParser.addFlag(
      'track-widget-creation',
      hide: !hasEffect && !verboseHelp,
1106
      defaultsTo: true,
1107 1108
      help: 'Track widget creation locations. This enables features such as the widget inspector. '
            'This parameter is only functional in debug mode (i.e. when compiling JIT, not AOT).',
1109 1110 1111
    );
  }

1112 1113 1114
  void usesAnalyzeSizeFlag() {
    argParser.addFlag(
      FlutterOptions.kAnalyzeSize,
1115
      help: 'Whether to produce additional profile information for artifact output size. '
1116 1117 1118
            'This flag is only supported on "--release" builds. When building for Android, a single '
            'ABI must be specified at a time with the "--target-platform" flag. When building for iOS, '
            'only the symbols from the arm64 architecture are used to analyze code size.\n'
1119
            'By default, the intermediate output files will be placed in a transient directory in the '
1120
            'build directory. This can be overridden with the "--${FlutterOptions.kCodeSizeDirectory}" option.\n'
1121
            'This flag cannot be combined with "--${FlutterOptions.kSplitDebugInfoOption}".'
1122
    );
1123 1124 1125 1126 1127 1128

    argParser.addOption(
      FlutterOptions.kCodeSizeDirectory,
      help: 'The location to write code size analysis files. If this is not specified, files '
            'are written to a temporary directory under the build directory.'
    );
1129 1130
  }

1131 1132 1133
  void addEnableImpellerFlag({required bool verboseHelp}) {
    argParser.addFlag('enable-impeller',
        hide: !verboseHelp,
1134 1135 1136 1137 1138 1139
        defaultsTo: null,
        help: 'Whether to enable the Impeller rendering engine. '
              'Impeller is the default renderer on iOS. On Android, Impeller '
              'is available but not the default. This flag will cause Impeller '
              'to be used on Android. On other platforms, this flag will be '
              'ignored.',
1140 1141 1142
    );
  }

1143 1144 1145 1146 1147 1148 1149 1150 1151
  void addEnableVulkanValidationFlag({required bool verboseHelp}) {
    argParser.addFlag('enable-vulkan-validation',
        hide: !verboseHelp,
        help: 'Enable vulkan validation on the Impeller rendering backend if '
              'Vulkan is in use and the validation layers are available to the '
              'application.',
    );
  }

1152 1153 1154 1155 1156 1157 1158
  void addEnableEmbedderApiFlag({required bool verboseHelp}) {
    argParser.addFlag('enable-embedder-api',
        hide: !verboseHelp,
        help: 'Whether to enable the experimental embedder API on iOS.',
    );
  }

1159
  /// Compute the [BuildInfo] for the current flutter command.
1160 1161
  /// Commands that build multiple build modes can pass in a [forcedBuildMode]
  /// to be used instead of parsing flags.
1162 1163
  ///
  /// Throws a [ToolExit] if the current set of options is not compatible with
1164
  /// each other.
1165
  Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async {
1166
    final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') &&
1167
      boolArg('track-widget-creation');
1168

1169
    final String? buildNumber = argParser.options.containsKey('build-number')
1170
      ? stringArg('build-number')
1171
      : null;
1172

1173
    final File packagesFile = globals.fs.file(
1174
      packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'));
1175 1176 1177
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
        packagesFile, logger: globals.logger, throwOnError: false);

1178 1179
    final List<String> experiments =
      argParser.options.containsKey(FlutterOptions.kEnableExperiment)
1180
        ? stringsArg(FlutterOptions.kEnableExperiment).toList()
1181 1182 1183
        : <String>[];
    final List<String> extraGenSnapshotOptions =
      argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
1184
        ? stringsArg(FlutterOptions.kExtraGenSnapshotOptions).toList()
1185
        : <String>[];
1186
    final List<String> extraFrontEndOptions =
1187
      argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
1188
          ? stringsArg(FlutterOptions.kExtraFrontEndOptions).toList()
1189 1190 1191 1192
          : <String>[];

    if (experiments.isNotEmpty) {
      for (final String expFlag in experiments) {
1193
        final String flag = '--enable-experiment=$expFlag';
1194
        extraFrontEndOptions.add(flag);
1195
        extraGenSnapshotOptions.add(flag);
1196 1197 1198
      }
    }

1199
    String? codeSizeDirectory;
1200
    if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) && boolArg(FlutterOptions.kAnalyzeSize)) {
1201
      Directory directory = globals.fsUtils.getUniqueDirectory(
1202 1203 1204
        globals.fs.directory(getBuildDirectory()),
        'flutter_size',
      );
1205 1206
      if (argParser.options.containsKey(FlutterOptions.kCodeSizeDirectory) && stringArg(FlutterOptions.kCodeSizeDirectory) != null) {
        directory = globals.fs.directory(stringArg(FlutterOptions.kCodeSizeDirectory));
1207
      }
1208 1209
      directory.createSync(recursive: true);
      codeSizeDirectory = directory.path;
1210 1211
    }

1212 1213 1214
    NullSafetyMode nullSafetyMode = NullSafetyMode.sound;
    if (argParser.options.containsKey(FlutterOptions.kNullSafety)) {
      final bool wasNullSafetyFlagParsed = argResults?.wasParsed(FlutterOptions.kNullSafety) ?? false;
1215 1216 1217 1218
      // Extra frontend options are only provided if explicitly
      // requested.
      if (wasNullSafetyFlagParsed) {
        if (boolArg(FlutterOptions.kNullSafety)) {
1219
          nullSafetyMode = NullSafetyMode.sound;
1220
          extraFrontEndOptions.add('--sound-null-safety');
1221
        } else {
1222 1223
          nullSafetyMode = NullSafetyMode.unsound;
          extraFrontEndOptions.add('--no-sound-null-safety');
1224 1225 1226 1227
        }
      }
    }

1228
    final bool dartObfuscation = argParser.options.containsKey(FlutterOptions.kDartObfuscationOption)
1229
      && boolArg(FlutterOptions.kDartObfuscationOption);
1230

1231
    final String? splitDebugInfoPath = argParser.options.containsKey(FlutterOptions.kSplitDebugInfoOption)
1232
      ? stringArg(FlutterOptions.kSplitDebugInfoOption)
1233 1234
      : null;

1235
    final bool androidGradleDaemon = !argParser.options.containsKey(FlutterOptions.kAndroidGradleDaemon)
1236
      || boolArg(FlutterOptions.kAndroidGradleDaemon);
1237

1238 1239 1240
    final bool androidSkipBuildDependencyValidation = !argParser.options.containsKey(FlutterOptions.kAndroidSkipBuildDependencyValidation)
        || boolArg(FlutterOptions.kAndroidSkipBuildDependencyValidation);

1241 1242 1243 1244
    final List<String> androidProjectArgs = argParser.options.containsKey(FlutterOptions.kAndroidProjectArgs)
      ? stringsArg(FlutterOptions.kAndroidProjectArgs)
      : <String>[];

1245 1246 1247 1248 1249 1250
    if (dartObfuscation && (splitDebugInfoPath == null || splitDebugInfoPath.isEmpty)) {
      throwToolExit(
        '"--${FlutterOptions.kDartObfuscationOption}" can only be used in '
        'combination with "--${FlutterOptions.kSplitDebugInfoOption}"',
      );
    }
1251
    final BuildMode buildMode = forcedBuildMode ?? getBuildMode();
1252
    if (buildMode != BuildMode.release && codeSizeDirectory != null) {
1253
      throwToolExit('"--${FlutterOptions.kAnalyzeSize}" can only be used on release builds.');
1254
    }
1255
    if (codeSizeDirectory != null && splitDebugInfoPath != null) {
1256
      throwToolExit('"--${FlutterOptions.kAnalyzeSize}" cannot be combined with "--${FlutterOptions.kSplitDebugInfoOption}".');
1257
    }
1258

1259
    final bool treeShakeIcons = argParser.options.containsKey('tree-shake-icons')
1260
      && buildMode.isPrecompiled
1261
      && boolArg('tree-shake-icons');
1262

1263
    final String? bundleSkSLPath = argParser.options.containsKey(FlutterOptions.kBundleSkSLPathOption)
1264
      ? stringArg(FlutterOptions.kBundleSkSLPathOption)
1265 1266
      : null;

1267 1268 1269 1270
    if (bundleSkSLPath != null && !globals.fs.isFileSync(bundleSkSLPath)) {
      throwToolExit('No SkSL shader bundle found at $bundleSkSLPath.');
    }

1271
    final String? performanceMeasurementFile = argParser.options.containsKey(FlutterOptions.kPerformanceMeasurementFile)
1272
      ? stringArg(FlutterOptions.kPerformanceMeasurementFile)
1273 1274
      : null;

1275
    final Map<String, Object?> defineConfigJsonMap = extractDartDefineConfigJsonMap();
1276
    final List<String> dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap);
1277

1278 1279 1280 1281 1282 1283 1284 1285 1286
    if (argParser.options.containsKey(FlutterOptions.kWebResourcesCdnFlag)) {
      final bool hasLocalWebSdk = argParser.options.containsKey('local-web-sdk') && stringArg('local-web-sdk') != null;
      if (boolArg(FlutterOptions.kWebResourcesCdnFlag) && !hasLocalWebSdk) {
        if (!dartDefines.any((String define) => define.startsWith('FLUTTER_WEB_CANVASKIT_URL='))) {
          dartDefines.add('FLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/${globals.flutterVersion.engineRevision}/');
        }
      }
    }

1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298
    final String? flavor = argParser.options.containsKey('flavor') ? stringArg('flavor') : null;
    if (flavor != null) {
      if (globals.platform.environment['FLUTTER_APP_FLAVOR'] != null) {
        throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.');
      }
      if (dartDefines.any((String define) => define.startsWith('FLUTTER_APP_FLAVOR'))) {
        throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be '
          'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}');
      }
      dartDefines.add('FLUTTER_APP_FLAVOR=$flavor');
    }

1299
    return BuildInfo(buildMode,
1300
      flavor,
1301
      trackWidgetCreation: trackWidgetCreation,
1302 1303 1304 1305
      frontendServerStarterPath: argParser.options
              .containsKey(FlutterOptions.kFrontendServerStarterPath)
          ? stringArg(FlutterOptions.kFrontendServerStarterPath)
          : null,
1306
      extraFrontEndOptions: extraFrontEndOptions.isNotEmpty
1307 1308
        ? extraFrontEndOptions
        : null,
1309
      extraGenSnapshotOptions: extraGenSnapshotOptions.isNotEmpty
1310
        ? extraGenSnapshotOptions
1311
        : null,
1312 1313
      fileSystemRoots: fileSystemRoots,
      fileSystemScheme: fileSystemScheme,
1314 1315
      buildNumber: buildNumber,
      buildName: argParser.options.containsKey('build-name')
1316
          ? stringArg('build-name')
1317
          : null,
1318
      treeShakeIcons: treeShakeIcons,
1319 1320
      splitDebugInfoPath: splitDebugInfoPath,
      dartObfuscation: dartObfuscation,
1321
      dartDefines: dartDefines,
1322
      bundleSkSLPath: bundleSkSLPath,
1323
      dartExperiments: experiments,
1324
      performanceMeasurementFile: performanceMeasurementFile,
1325
      packagesPath: packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'),
1326
      nullSafetyMode: nullSafetyMode,
1327
      codeSizeDirectory: codeSizeDirectory,
1328
      androidGradleDaemon: androidGradleDaemon,
1329
      androidSkipBuildDependencyValidation: androidSkipBuildDependencyValidation,
1330
      packageConfig: packageConfig,
1331
      androidProjectArgs: androidProjectArgs,
1332
      initializeFromDill: argParser.options.containsKey(FlutterOptions.kInitializeFromDill)
1333
          ? stringArg(FlutterOptions.kInitializeFromDill)
1334
          : null,
1335
      assumeInitializeFromDillUpToDate: argParser.options.containsKey(FlutterOptions.kAssumeInitializeFromDillUpToDate)
1336
          && boolArg(FlutterOptions.kAssumeInitializeFromDillUpToDate),
1337
    );
1338 1339
  }

1340
  void setupApplicationPackages() {
1341
    applicationPackages ??= ApplicationPackageFactory.instance;
1342 1343
  }

1344
  /// The path to send to Google Analytics. Return null here to disable
1345
  /// tracking of the command.
1346
  Future<String?> get usagePath async {
1347
    if (parent is FlutterCommand) {
1348 1349
      final FlutterCommand? commandParent = parent as FlutterCommand?;
      final String? path = await commandParent?.usagePath;
1350 1351 1352 1353 1354 1355
      // Don't report for parents that return null for usagePath.
      return path == null ? null : '$path/$name';
    } else {
      return name;
    }
  }
1356

1357
  /// Additional usage values to be sent with the usage ping.
1358
  Future<CustomDimensions> get usageValues async => const CustomDimensions();
1359

1360 1361 1362 1363 1364 1365 1366 1367
  /// 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);

1368 1369 1370 1371 1372 1373
  /// 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.
1374
  @override
1375
  Future<void> run() {
1376
    final DateTime startTime = globals.systemClock.now();
Devon Carew's avatar
Devon Carew committed
1377

1378
    return context.run<void>(
1379 1380 1381
      name: 'command',
      overrides: <Type, Generator>{FlutterCommand: () => this},
      body: () async {
1382
        if (_usesFatalWarnings) {
1383
          globals.logger.fatalWarnings = boolArg(FlutterOptions.kFatalWarnings);
1384
        }
1385
        // Prints the welcome message if needed.
1386
        globals.flutterUsage.printWelcome();
1387
        _printDeprecationWarning();
1388 1389 1390 1391
        final String? commandPath = await usagePath;
        if (commandPath != null) {
          _registerSignalHandlers(commandPath, startTime);
        }
1392
        FlutterCommandResult commandResult = FlutterCommandResult.fail();
1393
        try {
1394
          commandResult = await verifyThenRunCommand(commandPath);
1395
        } finally {
1396
          final DateTime endTime = globals.systemClock.now();
1397
          globals.printTrace(globals.userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
1398
          if (commandPath != null) {
1399 1400 1401 1402 1403 1404
            _sendPostUsage(
              commandPath,
              commandResult,
              startTime,
              endTime,
            );
1405
          }
1406 1407 1408
          if (_usesFatalWarnings) {
            globals.logger.checkForFatalLogs();
          }
1409 1410 1411
        }
      },
    );
Devon Carew's avatar
Devon Carew committed
1412 1413
  }

1414 1415 1416 1417 1418 1419 1420 1421
  @visibleForOverriding
  String get deprecationWarning {
    return '${globals.logger.terminal.warningMark} The "$name" command is '
           'deprecated and will be removed in a future version of Flutter. '
           'See https://flutter.dev/docs/development/tools/sdk/releases '
           'for previous releases of Flutter.\n';
  }

1422 1423
  void _printDeprecationWarning() {
    if (deprecated) {
1424
      globals.printWarning(deprecationWarning);
1425 1426 1427
    }
  }

1428
  List<String> extractDartDefines({required Map<String, Object?> defineConfigJsonMap}) {
1429 1430
    final List<String> dartDefines = <String>[];

1431
    defineConfigJsonMap.forEach((String key, Object? value) {
1432 1433 1434
      dartDefines.add('$key=$value');
    });

1435 1436 1437 1438
    if (argParser.options.containsKey(FlutterOptions.kDartDefinesOption)) {
      dartDefines.addAll(stringsArg(FlutterOptions.kDartDefinesOption));
    }

1439 1440 1441
    return dartDefines;
  }

1442 1443
  Map<String, Object?> extractDartDefineConfigJsonMap() {
    final Map<String, Object?> dartDefineConfigJsonMap = <String, Object?>{};
1444 1445

    if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) {
1446
      final List<String> configFilePaths = stringsArg(
1447
        FlutterOptions.kDartDefineFromFileOption,
1448 1449
      );

1450
      for (final String path in configFilePaths) {
1451
        if (!globals.fs.isFileSync(path)) {
1452 1453
          throwToolExit('Did not find the file passed to "--${FlutterOptions
              .kDartDefineFromFileOption}". Path: $path');
1454 1455
        }

1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467
        final String configRaw = globals.fs.file(path).readAsStringSync();

        // 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);
        }

1468 1469 1470
        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>)
1471 1472
              .forEach((String key, Object? value) {
            dartDefineConfigJsonMap[key] = value;
1473 1474
          });
        } on FormatException catch (err) {
1475 1476 1477 1478
          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'
          );
1479 1480 1481 1482 1483 1484
        }
      }
    }

    return dartDefineConfigJsonMap;
  }
1485 1486 1487 1488 1489 1490 1491 1492 1493 1494

  /// 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) {
1495
    if (DotEnvRegex.multiLineBlock.hasMatch(line)) {
1496 1497 1498
      throwToolExit('Multi-line value is not supported: $line');
    }

1499 1500
    final Match? keyValueMatch = DotEnvRegex.keyValue.firstMatch(line);
    if (keyValueMatch == null) {
1501 1502 1503 1504 1505
      throwToolExit('Unable to parse file provided for '
        '--${FlutterOptions.kDartDefineFromFileOption}.\n'
        'Invalid property line: $line');
    }

1506 1507
    final String key = keyValueMatch.group(1)!;
    final String value = keyValueMatch.group(2) ?? '';
1508 1509

    // Remove wrapping quotes and trailing line comment.
1510 1511 1512
    final Match? doubleQuotedValueMatch = DotEnvRegex.doubleQuotedValue.firstMatch(value);
    if (doubleQuotedValueMatch != null) {
      return MapEntry<String, String>(key, doubleQuotedValueMatch.group(1)!);
1513 1514
    }

1515 1516 1517
    final Match? singleQuotedValueMatch = DotEnvRegex.singleQuotedValue.firstMatch(value);
    if (singleQuotedValueMatch != null) {
      return MapEntry<String, String>(key, singleQuotedValueMatch.group(1)!);
1518 1519
    }

1520 1521 1522
    final Match? backQuotedValueMatch = DotEnvRegex.backQuotedValue.firstMatch(value);
    if (backQuotedValueMatch != null) {
      return MapEntry<String, String>(key, backQuotedValueMatch.group(1)!);
1523 1524
    }

1525 1526 1527
    final Match? unquotedValueMatch = DotEnvRegex.unquotedValue.firstMatch(value);
    if (unquotedValueMatch != null) {
      return MapEntry<String, String>(key, unquotedValueMatch.group(1)!);
1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560
    }

    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 Map<String, String> propertyMap = <String, String>{};
    for (final String line in lines) {
      final MapEntry<String, String> property = _parseProperty(line);
      propertyMap[property.key] = property.value;
    }

    return jsonEncode(propertyMap);
  }
1561

1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585
  Map<String, String> extractWebHeaders() {
    final Map<String, String> webHeaders = <String, String>{};

    if (argParser.options.containsKey('web-header')) {
      final List<String> candidates = stringsArg('web-header');
      final List<String> invalidHeaders = <String>[];
      for (final String 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;
  }

1586
  void _registerSignalHandlers(String commandPath, DateTime startTime) {
1587
    void handler(io.ProcessSignal s) {
1588
      globals.cache.releaseLock();
1589 1590 1591 1592
      _sendPostUsage(
        commandPath,
        const FlutterCommandResult(ExitStatus.killed),
        startTime,
1593
        globals.systemClock.now(),
1594
      );
1595
    }
1596 1597
    globals.signals.addHandler(io.ProcessSignal.sigterm, handler);
    globals.signals.addHandler(io.ProcessSignal.sigint, handler);
1598 1599
  }

1600 1601 1602 1603
  /// 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.
1604 1605 1606 1607 1608 1609
  void _sendPostUsage(
    String commandPath,
    FlutterCommandResult commandResult,
    DateTime startTime,
    DateTime endTime,
  ) {
1610
    // Send command result.
1611 1612 1613 1614 1615 1616
    final int? maxRss = getMaxRss(processInfo);
    CommandResultEvent(commandPath, commandResult.toString(), maxRss).send();
    analytics.send(Event.flutterCommandResult(
      commandPath: commandPath,
      result: commandResult.toString(),
      maxRss: maxRss,
1617
      commandHasTerminal: hasTerminal,
1618
    ));
1619 1620

    // Send timing.
1621
    final List<String?> labels = <String?>[
1622
      commandResult.exitStatus.name,
1623
      if (commandResult.timingLabelParts?.isNotEmpty ?? false)
1624
        ...?commandResult.timingLabelParts,
1625
    ];
1626 1627

    final String label = labels
1628
        .where((String? label) => label != null && !_isBlank(label))
1629
        .join('-');
1630 1631 1632 1633

    // 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);
1634
    globals.flutterUsage.sendTiming(
1635 1636
      'flutter',
      name,
1637
      elapsedDuration,
1638 1639 1640 1641
      // 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,
    );
1642 1643 1644 1645 1646 1647 1648 1649
    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,
    ));
1650 1651
  }

1652 1653 1654 1655 1656 1657 1658 1659
  /// 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
1660
  Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async {
1661 1662 1663 1664 1665 1666 1667 1668 1669 1670
    if (argParser.options.containsKey(FlutterOptions.kNullSafety) &&
        argResults![FlutterOptions.kNullSafety] == false &&
        globals.nonNullSafeBuilds == NonNullSafeBuilds.notAllowed) {
      throwToolExit('''
Could not find an option named "no-${FlutterOptions.kNullSafety}".

Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and options.
''');
    }

1671
    globals.preRunValidator.validate();
1672 1673 1674 1675 1676 1677 1678 1679

    if (refreshWirelessDevices) {
      // Loading wireless devices takes longer so start it early.
      _targetDevices.startExtendedWirelessDeviceDiscovery(
        deviceDiscoveryTimeout: deviceDiscoveryTimeout,
      );
    }

1680 1681
    // 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.
1682
    if (shouldUpdateCache) {
1683
      // First always update universal artifacts, as some of these (e.g.
1684
      // ios-deploy on macOS) are required to determine `requiredArtifacts`.
1685
      final bool offline;
1686
      if (argParser.options.containsKey('offline')) {
1687
        offline = boolArg('offline');
1688
      } else {
1689 1690 1691 1692
        offline = false;
      }
      await globals.cache.updateAll(<DevelopmentArtifact>{DevelopmentArtifact.universal}, offline: offline);
      await globals.cache.updateAll(await requiredArtifacts, offline: offline);
1693
    }
1694
    globals.cache.releaseLock();
1695

1696 1697
    await validateCommand();

1698 1699 1700
    final FlutterProject project = FlutterProject.current();
    project.checkForDeprecation(deprecationBehavior: deprecationBehavior);

1701
    if (shouldRunPub) {
1702
      final Environment environment = Environment(
1703
        artifacts: globals.artifacts!,
1704 1705 1706 1707 1708 1709 1710
        logger: globals.logger,
        cacheDir: globals.cache.getRoot(),
        engineVersion: globals.flutterVersion.engineRevision,
        fileSystem: globals.fs,
        flutterRootDir: globals.fs.directory(Cache.flutterRoot),
        outputDir: globals.fs.directory(getBuildDirectory()),
        processManager: globals.processManager,
1711
        platform: globals.platform,
1712
        usage: globals.flutterUsage,
1713
        analytics: analytics,
1714
        projectDir: project.directory,
1715
        generateDartPluginRegistry: true,
1716 1717 1718 1719
      );

      await generateLocalizationsSyntheticPackage(
        environment: environment,
1720
        buildSystem: globals.buildSystem,
1721
        buildTargets: globals.buildTargets,
1722 1723
      );

1724 1725
      await pub.get(
        context: PubContext.getVerifyContext(name),
1726
        project: project,
1727
        checkUpToDate: cachePubGet,
1728
      );
1729 1730 1731 1732 1733 1734 1735 1736

      // null implicitly means all plugins are allowed
      List<String>? allowedPlugins;
      if (stringArg(FlutterGlobalOptions.kDeviceIdOption, global: true) == 'preview') {
        // The preview device does not currently support any plugins.
        allowedPlugins = PreviewDevice.supportedPubPlugins;
      }
      await project.regeneratePlatformSpecificTooling(allowedPlugins: allowedPlugins);
1737 1738 1739
      if (reportNullSafety) {
        await _sendNullSafetyAnalyticsEvents(project);
      }
1740
    }
1741

1742
    setupApplicationPackages();
Devon Carew's avatar
Devon Carew committed
1743

1744
    if (commandPath != null) {
1745 1746 1747 1748 1749 1750 1751
      // Until the GA4 migration is complete, we will continue to send to the GA3 instance
      // as well as GA4. Once migration is complete, we will only make a call for GA4 values
      final List<Object> pairOfUsageValues = await Future.wait<Object>(<Future<Object>>[
        usageValues,
        unifiedAnalyticsUsageValues(commandPath),
      ]);

1752
      Usage.command(commandPath, parameters: CustomDimensions(
1753 1754 1755
        commandHasTerminal: hasTerminal,
      ).merge(pairOfUsageValues[0] as CustomDimensions));
      analytics.send(pairOfUsageValues[1] as Event);
1756 1757
    }

1758
    return runCommand();
1759 1760
  }

1761 1762 1763 1764 1765 1766 1767 1768 1769 1770
  Future<void> _sendNullSafetyAnalyticsEvents(FlutterProject project) async {
    final BuildInfo buildInfo = await getBuildInfo();
    NullSafetyAnalysisEvent(
      buildInfo.packageConfig,
      buildInfo.nullSafetyMode,
      project.manifest.appName,
      globals.flutterUsage,
    ).send();
  }

1771 1772
  /// The set of development artifacts required for this command.
  ///
1773 1774 1775
  /// Defaults to an empty set. Including [DevelopmentArtifact.universal] is
  /// not required as it is always updated.
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
1776

1777
  /// Subclasses must implement this to execute the command.
1778
  /// Optionally provide a [FlutterCommandResult] to send more details about the
1779 1780
  /// execution for analytics.
  Future<FlutterCommandResult> runCommand();
1781

1782
  /// Find and return all target [Device]s based upon currently connected
1783
  /// devices and criteria entered by the user on the command line.
1784
  /// If no device can be found that meets specified criteria,
1785
  /// then print an error message and return null.
1786
  Future<List<Device>?> findAllTargetDevices({
1787
    bool includeDevicesUnsupportedByProject = false,
1788
  }) async {
1789 1790
    return _targetDevices.findAllTargetDevices(
      deviceDiscoveryTimeout: deviceDiscoveryTimeout,
1791
      includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
1792 1793 1794
    );
  }

1795 1796 1797
  /// 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,
1798
  /// then print an error message and return null.
1799
  ///
1800
  /// If [includeDevicesUnsupportedByProject] is true, the tool does not filter
1801
  /// the list by the current project support list.
1802
  Future<Device?> findTargetDevice({
1803
    bool includeDevicesUnsupportedByProject = false,
1804
  }) async {
1805 1806 1807
    List<Device>? deviceList = await findAllTargetDevices(
      includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
    );
1808
    if (deviceList == null) {
1809
      return null;
1810
    }
1811
    if (deviceList.length > 1) {
1812
      globals.printStatus(globals.userMessages.flutterSpecifyDevice);
1813
      deviceList = await globals.deviceManager!.getAllDevices();
1814
      globals.printStatus('');
1815
      await Device.printDevices(deviceList, globals.logger);
1816 1817 1818
      return null;
    }
    return deviceList.single;
1819 1820
  }

1821 1822
  @protected
  @mustCallSuper
1823
  Future<void> validateCommand() async {
1824
    if (_requiresPubspecYaml && globalResults?.wasParsed('packages') != true) {
1825
      // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
1826 1827 1828

      // If there is no pubspec in the current directory, look in the parent
      // until one can be found.
1829 1830
      final String? path = findProjectRoot(globals.fs, globals.fs.currentDirectory.path);
      if (path == null) {
1831
        throwToolExit(globals.userMessages.flutterNoPubspec);
1832
      }
1833 1834
      if (path != globals.fs.currentDirectory.path) {
        globals.fs.currentDirectory = path;
1835
        globals.printStatus('Changing current working directory to: ${globals.fs.currentDirectory.path}');
1836
      }
1837
    }
1838 1839

    if (_usesTargetOption) {
1840
      final String targetPath = targetFile;
1841
      if (!globals.fs.isFileSync(targetPath)) {
1842
        throw ToolExit(globals.userMessages.flutterTargetFileMissing(targetPath));
1843
      }
1844 1845
    }
  }
1846

1847 1848 1849 1850 1851 1852 1853
  @override
  String get usage {
    final String usageWithoutDescription = super.usage.substring(
      // The description plus two newlines.
      description.length + 2,
    );
    final String help = <String>[
1854
      if (deprecated)
1855
        '${globals.logger.terminal.warningMark} Deprecated. This command will be removed in a future version of Flutter.',
1856 1857 1858
      description,
      '',
      'Global options:',
1859
      '${runner?.argParser.usage}',
1860 1861 1862 1863 1864 1865
      '',
      usageWithoutDescription,
    ].join('\n');
    return help;
  }

1866
  ApplicationPackageFactory? applicationPackages;
1867

1868 1869 1870 1871
  /// 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.
1872 1873 1874 1875 1876 1877
  bool boolArg(String name, {bool global = false}) {
    if (global) {
      return globalResults![name] as bool;
    }
    return argResults![name] as bool;
  }
1878

1879
  /// Gets the parsed command-line option named [name] as a `String`.
1880 1881 1882
  ///
  /// If no option named [name] was added to the [ArgParser], an [ArgumentError]
  /// will be thrown.
1883 1884 1885 1886 1887 1888
  String? stringArg(String name, {bool global = false}) {
    if (global) {
      return globalResults![name] as String?;
    }
    return argResults![name] as String?;
  }
1889 1890

  /// Gets the parsed command-line option named [name] as `List<String>`.
1891 1892 1893 1894 1895
  List<String> stringsArg(String name, {bool global = false}) {
    if (global) {
      return globalResults![name] as List<String>;
    }
    return argResults![name] as List<String>;
1896
  }
1897
}
1898

1899
/// A mixin which applies an implementation of [requiredArtifacts] that only
1900
/// downloads artifacts corresponding to potentially connected devices.
1901 1902 1903
mixin DeviceBasedDevelopmentArtifacts on FlutterCommand {
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
1904 1905 1906 1907 1908 1909 1910
    // 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),
    );
1911 1912 1913 1914 1915 1916
    if (devices.isEmpty) {
      return super.requiredArtifacts;
    }
    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
      DevelopmentArtifact.universal,
    };
1917
    for (final Device device in devices) {
1918
      final TargetPlatform targetPlatform = await device.targetPlatform;
1919
      final DevelopmentArtifact? developmentArtifact = artifactFromTargetPlatform(targetPlatform);
1920 1921
      if (developmentArtifact != null) {
        artifacts.add(developmentArtifact);
1922 1923 1924 1925 1926 1927
      }
    }
    return artifacts;
  }
}

1928 1929
// Returns the development artifact for the target platform, or null
// if none is supported
1930
@protected
1931
DevelopmentArtifact? artifactFromTargetPlatform(TargetPlatform targetPlatform) {
1932
  switch (targetPlatform) {
1933
    case TargetPlatform.android:
1934 1935 1936 1937
    case TargetPlatform.android_arm:
    case TargetPlatform.android_arm64:
    case TargetPlatform.android_x64:
    case TargetPlatform.android_x86:
1938
      return DevelopmentArtifact.androidGenSnapshot;
1939
    case TargetPlatform.web_javascript:
1940 1941 1942
      return DevelopmentArtifact.web;
    case TargetPlatform.ios:
      return DevelopmentArtifact.iOS;
1943
    case TargetPlatform.darwin:
1944
      if (featureFlags.isMacOSEnabled) {
1945 1946 1947
        return DevelopmentArtifact.macOS;
      }
      return null;
1948
    case TargetPlatform.windows_x64:
1949
    case TargetPlatform.windows_arm64:
1950
      if (featureFlags.isWindowsEnabled) {
1951 1952 1953
        return DevelopmentArtifact.windows;
      }
      return null;
1954
    case TargetPlatform.linux_x64:
1955
    case TargetPlatform.linux_arm64:
1956
      if (featureFlags.isLinuxEnabled) {
1957 1958 1959
        return DevelopmentArtifact.linux;
      }
      return null;
1960 1961
    case TargetPlatform.fuchsia_arm64:
    case TargetPlatform.fuchsia_x64:
1962 1963 1964 1965
    case TargetPlatform.tester:
      return null;
  }
}
1966 1967

/// Returns true if s is either null, empty or is solely made of whitespace characters (as defined by String.trim).
1968
bool _isBlank(String s) => s.trim().isEmpty;
1969 1970 1971 1972 1973 1974 1975 1976 1977

/// Whether the tool should allow non-null safe builds.
///
/// The Dart SDK no longer supports non-null safe builds, so this value in the
/// tool's context should always be [NonNullSafeBuilds.notAllowed].
enum NonNullSafeBuilds {
  allowed,
  notAllowed,
}