flutter_command.dart 45.6 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 10

import '../application_package.dart';
11
import '../base/common.dart';
12
import '../base/context.dart';
13
import '../base/io.dart' as io;
14
import '../base/signals.dart';
15
import '../base/terminal.dart';
16
import '../base/user_messages.dart';
17
import '../base/utils.dart';
18
import '../build_info.dart';
19
import '../build_system/build_system.dart';
20
import '../build_system/targets/icon_tree_shaker.dart' show kIconTreeShakerEnabledDefault;
21
import '../bundle.dart' as bundle;
22
import '../cache.dart';
23
import '../dart/generate_synthetic_packages.dart';
24
import '../dart/package_map.dart';
25
import '../dart/pub.dart';
26
import '../device.dart';
27
import '../features.dart';
28
import '../globals.dart' as globals;
29
import '../project.dart';
30
import '../reporting/reporting.dart';
31 32
import 'flutter_command_runner.dart';

33 34
export '../cache.dart' show DevelopmentArtifact;

35 36 37 38
enum ExitStatus {
  success,
  warning,
  fail,
39
  killed,
40 41 42
}

/// [FlutterCommand]s' subclasses' [FlutterCommand.runCommand] can optionally
43
/// provide a [FlutterCommandResult] to furnish additional information for
44 45
/// analytics.
class FlutterCommandResult {
46
  const FlutterCommandResult(
47
    this.exitStatus, {
48
    this.timingLabelParts,
49
    this.endTimeOverride,
50
  });
51

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
  /// 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);
  }

67 68
  final ExitStatus exitStatus;

69
  /// Optional data that can be appended to the timing event.
70 71
  /// https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#timingLabel
  /// Do not add PII.
72
  final List<String> timingLabelParts;
73

74
  /// Optional epoch time when the command's non-interactive wait time is
75
  /// complete during the command's execution. Use to measure user perceivable
76 77
  /// latency without measuring user interaction time.
  ///
78
  /// [FlutterCommand] will automatically measure and report the command's
79
  /// complete time if not overridden.
80
  final DateTime endTimeOverride;
81 82 83 84 85 86 87 88 89 90

  @override
  String toString() {
    switch (exitStatus) {
      case ExitStatus.success:
        return 'success';
      case ExitStatus.warning:
        return 'warning';
      case ExitStatus.fail:
        return 'fail';
91 92
      case ExitStatus.killed:
        return 'killed';
93 94 95 96 97
      default:
        assert(false);
        return null;
    }
  }
98 99
}

100 101 102 103
/// Common flutter command line options.
class FlutterOptions {
  static const String kExtraFrontEndOptions = 'extra-front-end-options';
  static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options';
104
  static const String kEnableExperiment = 'enable-experiment';
105 106
  static const String kFileSystemRoot = 'filesystem-root';
  static const String kFileSystemScheme = 'filesystem-scheme';
107
  static const String kSplitDebugInfoOption = 'split-debug-info';
108
  static const String kDartObfuscationOption = 'obfuscate';
109
  static const String kDartDefinesOption = 'dart-define';
110
  static const String kBundleSkSLPathOption = 'bundle-sksl-path';
111
  static const String kPerformanceMeasurementFile = 'performance-measurement-file';
112
  static const String kNullSafety = 'sound-null-safety';
113
  static const String kDeviceUser = 'device-user';
114
  static const String kDeviceTimeout = 'device-timeout';
115
  static const String kAnalyzeSize = 'analyze-size';
116
  static const String kNullAssertions = 'null-assertions';
117 118
}

119
abstract class FlutterCommand extends Command<void> {
120 121 122
  /// The currently executing command (or sub-command).
  ///
  /// Will be `null` until the top-most command has begun execution.
123
  static FlutterCommand get current => context.get<FlutterCommand>();
124

125 126 127 128 129 130
  /// The option name for a custom observatory port.
  static const String observatoryPortOption = 'observatory-port';

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

131 132
  @override
  ArgParser get argParser => _argParser;
133 134
  final ArgParser _argParser = ArgParser(
    allowTrailingOptions: false,
135
    usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
136
  );
137

138
  @override
139
  FlutterCommandRunner get runner => super.runner as FlutterCommandRunner;
140

141 142
  bool _requiresPubspecYaml = false;

143 144 145 146
  /// Whether this command uses the 'target' option.
  bool _usesTargetOption = false;

  bool _usesPubOption = false;
147

148 149 150 151
  bool _usesPortOption = false;

  bool _usesIpv6Flag = false;

152
  bool get shouldRunPub => _usesPubOption && boolArg('pub');
153

154 155
  bool get shouldUpdateCache => true;

156 157 158 159 160
  bool get deprecated => false;

  @override
  bool get hidden => deprecated;

161 162
  bool _excludeDebug = false;

163 164
  BuildMode _defaultBuildMode;

165 166 167 168
  void requiresPubspecYaml() {
    _requiresPubspecYaml = true;
  }

169
  void usesWebOptions({ bool hide = true }) {
170
    argParser.addOption('web-hostname',
171
      defaultsTo: 'localhost',
172 173 174 175 176
      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.',
177 178
      hide: hide,
    );
179
    argParser.addOption('web-port',
180 181 182 183 184
      defaultsTo: null,
      help: 'The host port to serve the web application from. If not provided, the tool '
        'will select a random open port on the host.',
      hide: hide,
    );
185 186 187 188
    argParser.addOption('web-server-debug-protocol',
      allowed: <String>['sse', 'ws'],
      defaultsTo: 'sse',
      help: 'The protocol (SSE or WebSockets) to use for the debug service proxy '
189
      'when using the Web Server device and Dart Debug extension. '
190 191 192 193
      'This is useful for editors/debug adapters that do not support debugging '
      'over SSE (the default protocol for Web Server/Dart Debugger extension).',
      hide: hide,
    );
194 195 196 197 198 199 200 201 202
    argParser.addOption('web-server-debug-backend-protocol',
      allowed: <String>['sse', 'ws'],
      defaultsTo: 'sse',
      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.',
      hide: hide,
    );
203 204 205 206 207 208
    argParser.addFlag('web-allow-expose-url',
      defaultsTo: false,
      help: 'Enables daemon-to-editor requests (app.exposeUrl) for exposing URLs '
        'when running on remote machines.',
      hide: hide,
    );
209 210 211 212 213 214 215 216 217 218 219 220 221
    argParser.addFlag('web-run-headless',
      defaultsTo: false,
      help: 'Launches the browser in headless mode. Currently only Chrome '
        'supports this option.',
      hide: true,
    );
    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/).',
      hide: true,
    );
222
    argParser.addFlag('web-enable-expression-evaluation',
223
      defaultsTo: true,
224 225 226
      help: 'Enables expression evaluation in the debugger.',
      hide: hide,
    );
227 228
  }

229 230 231
  void usesTargetOption() {
    argParser.addOption('target',
      abbr: 't',
232
      defaultsTo: bundle.defaultMainPath,
233
      help: 'The main entry-point file of the application, as run on the device.\n'
234
            'If the --target option is omitted, but a file name is provided on '
235 236
            'the command line, then that is used instead.',
      valueHelp: 'path');
237 238 239
    _usesTargetOption = true;
  }

240
  String get targetFile {
241
    if (argResults.wasParsed('target')) {
242
      return stringArg('target');
243 244
    }
    if (argResults.rest.isNotEmpty) {
245
      return argResults.rest.first;
246 247
    }
    return bundle.defaultMainPath;
248 249
  }

250
  void usesPubOption({bool hide = false}) {
251 252
    argParser.addFlag('pub',
      defaultsTo: true,
253
      hide: hide,
254
      help: 'Whether to run "flutter pub get" before executing this command.');
255 256 257
    _usesPubOption = true;
  }

258 259 260 261
  /// Adds flags for using a specific filesystem root and scheme.
  ///
  /// [hide] indicates whether or not to hide these options when the user asks
  /// for help.
262
  void usesFilesystemOptions({ @required bool hide }) {
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    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 root in a virtual file system\n'
            'for compilation. Input file name should be specified as Uri in\n'
            'filesystem-scheme scheme. Use only in Dart 2 mode.\n'
            'Requires --output-dill option to be explicitly specified.\n',
      )
      ..addOption(FlutterOptions.kFileSystemScheme,
        defaultsTo: 'org-dartlang-root',
        hide: hide,
        help: 'Specify the scheme that is used for virtual file system used in\n'
            'compilation. See more details on filesystem-root option.\n',
      );
  }

283 284 285
  /// Adds options for connecting to the Dart VM observatory port.
  void usesPortOptions() {
    argParser.addOption(observatoryPortOption,
286
        help: '(deprecated use host-vmservice-port instead) '
287
              'Listen to the given port for an observatory debugger connection.\n'
288 289 290
              'Specifying port 0 (the default) will find a random free port.\nNote: '
              'if the Dart Development Service (DDS) is enabled, this will not be the port '
              'of the Observatory instance advertised on the command line.',
291
    );
292 293 294 295 296 297 298 299 300 301
    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.'
    );
302 303 304
    _usesPortOption = true;
  }

305
  void addDdsOptions({@required bool verboseHelp}) {
306 307 308 309
    argParser.addOption('dds-port',
      help: 'When this value is provided, the Dart Development Service (DDS) will be '
            'bound to the provided port.\nSpecifying port 0 (the default) will find '
            'a random free port.');
310 311 312 313 314 315 316 317 318 319 320 321
    argParser.addFlag(
      'disable-dds',
      hide: !verboseHelp,
      help: 'Disable the Dart Developer Service (DDS). This flag should only be provided'
            ' 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'
            'Note: passing this flag may degrade IDE functionality if a DDS instance is not'
            ' already connected to the target application.'
    );
  }

322 323 324 325 326 327 328 329 330
  bool get disableDds => boolArg('disable-dds');

  int get ddsPort {
    if (argResults.wasParsed('dds-port')) {
      return int.tryParse(stringArg('dds-port')) ?? 0;
    }
    return 0;
  }

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  /// Gets the vmservice port provided to in the 'observatory-port' or
  /// 'host-vmservice-port option.
  ///
  /// Only one of "host-vmservice-port" and "observatory-port" may be
  /// specified.
  ///
  /// If no port is set, returns null.
  int get hostVmservicePort {
    if (!_usesPortOption ||
        (argResults['observatory-port'] == null &&
      argResults['host-vmservice-port'] == null)) {
      return null;
    }
    if (argResults.wasParsed('observatory-port') &&
        argResults.wasParsed('host-vmservice-port')) {
      throwToolExit('Only one of "--observatory-port" and '
        '"--host-vmservice-port" may be specified.');
    }
    try {
350
      return int.parse(stringArg('observatory-port') ?? stringArg('host-vmservice-port'));
351 352 353 354 355 356 357
    } on FormatException catch (error) {
      throwToolExit('Invalid port for `--observatory-port/--host-vmservice-port`: $error');
    }
    return null;
  }

  /// Gets the vmservice port provided to in the 'device-vmservice-port' option.
358 359
  ///
  /// If no port is set, returns null.
360 361
  int get deviceVmservicePort {
    if (!_usesPortOption || argResults['device-vmservice-port'] == null) {
362 363 364
      return null;
    }
    try {
365
      return int.parse(stringArg('device-vmservice-port'));
366 367
    } on FormatException catch (error) {
      throwToolExit('Invalid port for `--device-vmservice-port`: $error');
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
    }
    return null;
  }

  void usesIpv6Flag() {
    argParser.addFlag(ipv6Flag,
      hide: true,
      negatable: false,
      help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool '
            'forwards the host port to a device port. Not used when the '
            '--debug-port flag is not set.',
    );
    _usesIpv6Flag = true;
  }

383
  bool get ipv6 => _usesIpv6Flag ? boolArg('ipv6') : null;
384

385 386
  void usesBuildNumberOption() {
    argParser.addOption('build-number',
387 388
        help: 'An identifier used as an internal version number.\n'
              'Each build must have a unique identifier to differentiate it from previous builds.\n'
389
              'It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.\n'
390 391
              "On Android it is used as 'versionCode'.\n"
              "On Xcode builds it is used as 'CFBundleVersion'",
392
    );
393 394 395 396 397 398
  }

  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'
399 400
              "On Android it is used as 'versionName'.\n"
              "On Xcode builds it is used as 'CFBundleShortVersionString'",
401 402 403
        valueHelp: 'x.y.z');
  }

404
  void usesDartDefineOption() {
405
    argParser.addMultiOption(
406 407 408 409
      FlutterOptions.kDartDefinesOption,
      help: 'Additional key-value pairs that will be available as constants '
            'from the String.fromEnvironment, bool.fromEnvironment, int.fromEnvironment, '
            'and double.fromEnvironment constructors.\n'
410
            'Multiple defines can be passed by repeating --dart-define multiple times.',
411
      valueHelp: 'foo=bar',
412 413 414
    );
  }

415 416 417 418 419 420
  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');
  }

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
  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'
    );
  }

  Duration get deviceDiscoveryTimeout {
    if (_deviceDiscoveryTimeout == null
        && argResults.options.contains(FlutterOptions.kDeviceTimeout)
        && argResults.wasParsed(FlutterOptions.kDeviceTimeout)) {
      final int timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout));
      if (timeoutSeconds == null) {
        throwToolExit( 'Could not parse --${FlutterOptions.kDeviceTimeout} argument. It must be an integer.');
      }
      _deviceDiscoveryTimeout = Duration(seconds: timeoutSeconds);
    }
    return _deviceDiscoveryTimeout;
  }
  Duration _deviceDiscoveryTimeout;

443 444 445 446
  void addBuildModeFlags({ bool defaultToRelease = true, bool verboseHelp = false, bool excludeDebug = false }) {
    // A release build must be the default if a debug build is not possible.
    assert(defaultToRelease || !excludeDebug);
    _excludeDebug = excludeDebug;
447
    defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug;
448

449 450 451 452 453
    if (!excludeDebug) {
      argParser.addFlag('debug',
        negatable: false,
        help: 'Build a debug version of your app${defaultToRelease ? '' : ' (default mode)'}.');
    }
454 455
    argParser.addFlag('profile',
      negatable: false,
456
      help: 'Build a version of your app specialized for performance profiling.');
457
    argParser.addFlag('release',
458
      negatable: false,
459
      help: 'Build a release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
460 461 462 463
    argParser.addFlag('jit-release',
      negatable: false,
      hide: !verboseHelp,
      help: 'Build a JIT release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
464 465
  }

466 467 468 469 470 471 472
  void addSplitDebugInfoOption() {
    argParser.addOption(FlutterOptions.kSplitDebugInfoOption,
      help: 'In a release build, this flag reduces application size by storing '
        '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 '
473
        "with this flag, the 'flutter symbolize' command with the right program "
474 475
        'symbol file is required to obtain a human readable stack trace.\n'
        'This flag cannot be combined with --analyze-size',
476
      valueHelp: 'v1.2.3/',
477 478 479
    );
  }

480 481 482 483 484 485 486 487
  void addDartObfuscationOption() {
    argParser.addFlag(FlutterOptions.kDartObfuscationOption,
      help: 'In a release build, this flag removes identifiers and replaces them '
        'with randomized values for the purposes of source code obfuscation. This '
        'flag must always be combined with "--split-debug-info" 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 '
488 489 490 491 492 493
        '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.'
494 495 496
    );
  }

497 498 499 500 501 502
  void addBundleSkSLPathOption({ @required bool hide }) {
    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,
503
      valueHelp: 'flutter_1.sksl'
504 505 506
    );
  }

507 508 509
  void addTreeShakeIconsFlag({
    bool enabledByDefault
  }) {
510 511
    argParser.addFlag('tree-shake-icons',
      negatable: true,
512 513
      defaultsTo: enabledByDefault
        ?? kIconTreeShakerEnabledDefault,
514 515 516 517
      help: 'Tree shake icon fonts so that only glyphs used by the application remain.',
    );
  }

Emmanuel Garcia's avatar
Emmanuel Garcia committed
518 519 520 521
  void addShrinkingFlag() {
    argParser.addFlag('shrink',
      negatable: true,
      defaultsTo: true,
522
      help: 'Whether to enable code shrinking on release mode. '
Emmanuel Garcia's avatar
Emmanuel Garcia committed
523 524 525
            'When enabling shrinking, you also benefit from obfuscation, '
            'which shortens the names of your app’s classes and members, '
            'and optimization, which applies more aggressive strategies to '
526
            'further reduce the size of your app. '
527
            'To learn more, see: https://developer.android.com/studio/build/shrink-code',
Emmanuel Garcia's avatar
Emmanuel Garcia committed
528 529 530
      );
  }

531
  void addNullSafetyModeOptions({ @required bool hide }) {
532
    argParser.addFlag(FlutterOptions.kNullSafety,
533 534 535
      help:
        'Whether to override the inferred null safety mode. This allows null-safe '
        'libraries to depend on un-migrated (non-null safe) libraries. By default, '
536 537 538
        'Flutter mobile & desktop applications will attempt to run at the null safety '
        'level of their entrypoint library (usually lib/main.dart). Flutter web '
        'applications will default to sound null-safety, unless specifically configured.',
539
      defaultsTo: null,
540
      hide: hide,
541
    );
542 543 544 545 546 547
    argParser.addFlag(FlutterOptions.kNullAssertions,
      help:
        'Perform additional null assertions on the boundaries of migrated and '
        'unmigrated code. This setting is not currently supported on desktop '
        'devices.'
    );
548 549
  }

550 551 552 553 554 555 556
  void usesExtraFrontendOptions() {
    argParser.addMultiOption(FlutterOptions.kExtraFrontEndOptions,
      splitCommas: true,
      hide: true,
    );
  }

557
  void usesFuchsiaOptions({ bool hide = false }) {
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
    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',
    );
  }

574
  void addEnableExperimentation({ @required bool hide }) {
575 576 577 578 579 580
    argParser.addMultiOption(
      FlutterOptions.kEnableExperiment,
      help:
        'The name of an experimental Dart feature to enable. For more info '
        'see: https://github.com/dart-lang/sdk/blob/master/docs/process/'
        'experimental-flags.md',
581
      hide: hide,
582 583 584
    );
  }

585 586 587 588 589 590 591 592 593
  void addBuildPerformanceFile({ bool hide = false }) {
    argParser.addOption(
      FlutterOptions.kPerformanceMeasurementFile,
      help:
        'The name of a file where flutter assemble performance and '
        'cachedness information will be written in a JSON format.'
    );
  }

594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  /// Adds build options common to all of the desktop build commands.
  void addCommonDesktopBuildOptions({ bool verboseHelp = false }) {
    addBuildModeFlags(verboseHelp: verboseHelp);
    addBuildPerformanceFile(hide: !verboseHelp);
    addBundleSkSLPathOption(hide: !verboseHelp);
    addDartObfuscationOption();
    addEnableExperimentation(hide: !verboseHelp);
    addNullSafetyModeOptions(hide: !verboseHelp);
    addSplitDebugInfoOption();
    addTreeShakeIconsFlag();
    usesAnalyzeSizeFlag();
    usesDartDefineOption();
    usesExtraFrontendOptions();
    usesPubOption();
    usesTargetOption();
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
  }

612 613
  set defaultBuildMode(BuildMode value) {
    _defaultBuildMode = value;
614 615
  }

616
  BuildMode getBuildMode() {
617 618
    // No debug when _excludeDebug is true.
    // If debug is not excluded, then take the command line flag.
619
    final bool debugResult = !_excludeDebug && boolArg('debug');
620 621 622 623 624 625
    final List<bool> modeFlags = <bool>[
      debugResult,
      boolArg('jit-release'),
      boolArg('profile'),
      boolArg('release'),
    ];
626
    if (modeFlags.where((bool flag) => flag).length > 1) {
627 628
      throw UsageException('Only one of --debug, --profile, --jit-release, '
                           'or --release can be specified.', null);
629
    }
630
    if (debugResult) {
631
      return BuildMode.debug;
632
    }
633
    if (boolArg('profile')) {
634 635
      return BuildMode.profile;
    }
636
    if (boolArg('release')) {
637 638
      return BuildMode.release;
    }
639 640 641
    if (boolArg('jit-release')) {
      return BuildMode.jitRelease;
    }
642
    return _defaultBuildMode;
643 644
  }

645 646 647 648
  void usesFlavorOption() {
    argParser.addOption(
      'flavor',
      help: 'Build a custom app flavor as defined by platform-specific build setup.\n'
649 650 651 652 653 654 655 656 657
            'Supports the use of product flavors in Android Gradle scripts, and '
            'the use of custom Xcode schemes.',
    );
  }

  void usesTrackWidgetCreation({ bool hasEffect = true, @required bool verboseHelp }) {
    argParser.addFlag(
      'track-widget-creation',
      hide: !hasEffect && !verboseHelp,
658
      defaultsTo: true,
659 660
      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).',
661 662 663
    );
  }

664 665 666 667
  void usesAnalyzeSizeFlag() {
    argParser.addFlag(
      FlutterOptions.kAnalyzeSize,
      defaultsTo: false,
668
      help: 'Whether to produce additional profile information for artifact output size. '
669 670
        '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, '
671 672
        'only the symbols from the arm64 architecture are used to analyze code size.\n'
        'This flag cannot be combined with --split-debug-info.'
673 674 675
    );
  }

676
  /// Compute the [BuildInfo] for the current flutter command.
677 678
  /// Commands that build multiple build modes can pass in a [forcedBuildMode]
  /// to be used instead of parsing flags.
679 680
  ///
  /// Throws a [ToolExit] if the current set of options is not compatible with
681 682
  /// each other.
  BuildInfo getBuildInfo({ BuildMode forcedBuildMode }) {
683 684
    final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') &&
      boolArg('track-widget-creation');
685

686
    final String buildNumber = argParser.options.containsKey('build-number')
687 688
      ? stringArg('build-number')
      : null;
689

690 691
    final List<String> experiments =
      argParser.options.containsKey(FlutterOptions.kEnableExperiment)
692
        ? stringsArg(FlutterOptions.kEnableExperiment).toList()
693 694 695
        : <String>[];
    final List<String> extraGenSnapshotOptions =
      argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
696
        ? stringsArg(FlutterOptions.kExtraGenSnapshotOptions).toList()
697
        : <String>[];
698
    final List<String> extraFrontEndOptions =
699
      argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
700
          ? stringsArg(FlutterOptions.kExtraFrontEndOptions).toList()
701 702 703 704
          : <String>[];

    if (experiments.isNotEmpty) {
      for (final String expFlag in experiments) {
705
        final String flag = '--enable-experiment=' + expFlag;
706
        extraFrontEndOptions.add(flag);
707
        extraGenSnapshotOptions.add(flag);
708 709 710
      }
    }

711 712 713 714 715 716 717 718
    String codeSizeDirectory;
    if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) && boolArg(FlutterOptions.kAnalyzeSize)) {
      final Directory directory = globals.fsUtils.getUniqueDirectory(
        globals.fs.directory(getBuildDirectory()),
        'flutter_size',
      );
      directory.createSync(recursive: true);
      codeSizeDirectory = directory.path;
719 720
    }

721
    NullSafetyMode nullSafetyMode = NullSafetyMode.unsound;
722 723 724 725 726 727
    if (argParser.options.containsKey(FlutterOptions.kNullSafety)) {
      final bool nullSafety = boolArg(FlutterOptions.kNullSafety);
      // Explicitly check for `true` and `false` so that `null` results in not
      // passing a flag. This will use the automatically detected null-safety
      // value based on the entrypoint
      if (nullSafety == true) {
728
        nullSafetyMode = NullSafetyMode.sound;
729 730
        extraFrontEndOptions.add('--sound-null-safety');
      } else if (nullSafety == false) {
731
        nullSafetyMode = NullSafetyMode.unsound;
732
        extraFrontEndOptions.add('--no-sound-null-safety');
733 734
      } else if (extraFrontEndOptions.contains('--enable-experiment=non-nullable')) {
        nullSafetyMode = NullSafetyMode.autodetect;
735 736 737
      }
    }

738 739 740 741 742 743 744 745 746 747 748 749 750
    final bool dartObfuscation = argParser.options.containsKey(FlutterOptions.kDartObfuscationOption)
      && boolArg(FlutterOptions.kDartObfuscationOption);

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

    if (dartObfuscation && (splitDebugInfoPath == null || splitDebugInfoPath.isEmpty)) {
      throwToolExit(
        '"--${FlutterOptions.kDartObfuscationOption}" can only be used in '
        'combination with "--${FlutterOptions.kSplitDebugInfoOption}"',
      );
    }
751
    final BuildMode buildMode = forcedBuildMode ?? getBuildMode();
752
    if (buildMode != BuildMode.release && codeSizeDirectory != null) {
753 754
      throwToolExit('--analyze-size can only be used on release builds.');
    }
755 756 757
    if (codeSizeDirectory != null && splitDebugInfoPath != null) {
      throwToolExit('--analyze-size cannot be combined with --split-debug-info.');
    }
758

759 760 761
    final bool treeShakeIcons = argParser.options.containsKey('tree-shake-icons')
      && buildMode.isPrecompiled
      && boolArg('tree-shake-icons');
762

763 764 765 766
    final String bundleSkSLPath = argParser.options.containsKey(FlutterOptions.kBundleSkSLPathOption)
      ? stringArg(FlutterOptions.kBundleSkSLPathOption)
      : null;

767 768 769 770
    final String performanceMeasurementFile = argParser.options.containsKey(FlutterOptions.kPerformanceMeasurementFile)
      ? stringArg(FlutterOptions.kPerformanceMeasurementFile)
      : null;

771
    return BuildInfo(buildMode,
772
      argParser.options.containsKey('flavor')
773
        ? stringArg('flavor')
774
        : null,
775
      trackWidgetCreation: trackWidgetCreation,
776 777 778
      extraFrontEndOptions: extraFrontEndOptions?.isNotEmpty ?? false
        ? extraFrontEndOptions
        : null,
779 780
      extraGenSnapshotOptions: extraGenSnapshotOptions?.isNotEmpty ?? false
        ? extraGenSnapshotOptions
781
        : null,
782
      fileSystemRoots: argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
783 784
          ? stringsArg(FlutterOptions.kFileSystemRoot)
          : null,
785
      fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
786 787
          ? stringArg(FlutterOptions.kFileSystemScheme)
          : null,
788 789
      buildNumber: buildNumber,
      buildName: argParser.options.containsKey('build-name')
790
          ? stringArg('build-name')
791
          : null,
792
      treeShakeIcons: treeShakeIcons,
793 794
      splitDebugInfoPath: splitDebugInfoPath,
      dartObfuscation: dartObfuscation,
795 796 797
      dartDefines: argParser.options.containsKey(FlutterOptions.kDartDefinesOption)
          ? stringsArg(FlutterOptions.kDartDefinesOption)
          : const <String>[],
798
      bundleSkSLPath: bundleSkSLPath,
799
      dartExperiments: experiments,
800
      performanceMeasurementFile: performanceMeasurementFile,
801 802
      packagesPath: globalResults['packages'] as String ?? '.packages',
      nullSafetyMode: nullSafetyMode,
803
      codeSizeDirectory: codeSizeDirectory,
804
    );
805 806
  }

807
  void setupApplicationPackages() {
808
    applicationPackages ??= ApplicationPackageStore();
809 810
  }

811
  /// The path to send to Google Analytics. Return null here to disable
812
  /// tracking of the command.
813 814
  Future<String> get usagePath async {
    if (parent is FlutterCommand) {
815
      final FlutterCommand commandParent = parent as FlutterCommand;
816 817 818 819 820 821 822
      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;
    }
  }
823

824
  /// Additional usage values to be sent with the usage ping.
825 826
  Future<Map<CustomDimensions, String>> get usageValues async =>
      const <CustomDimensions, String>{};
827

828 829 830 831 832 833
  /// 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.
834
  @override
835
  Future<void> run() {
836
    final DateTime startTime = globals.systemClock.now();
Devon Carew's avatar
Devon Carew committed
837

838
    return context.run<void>(
839 840 841
      name: 'command',
      overrides: <Type, Generator>{FlutterCommand: () => this},
      body: () async {
842
        // Prints the welcome message if needed.
843
        globals.flutterUsage.printWelcome();
844
        _printDeprecationWarning();
845
        final String commandPath = await usagePath;
846
        _registerSignalHandlers(commandPath, startTime);
847
        FlutterCommandResult commandResult = FlutterCommandResult.fail();
848
        try {
849
          commandResult = await verifyThenRunCommand(commandPath);
850
        } finally {
851
          final DateTime endTime = globals.systemClock.now();
852
          globals.printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
853
          _sendPostUsage(commandPath, commandResult, startTime, endTime);
854 855 856
        }
      },
    );
Devon Carew's avatar
Devon Carew committed
857 858
  }

859 860 861
  void _printDeprecationWarning() {
    if (deprecated) {
      globals.printStatus('$warningMark The "$name" command is deprecated and '
862 863 864
          'will be removed in a future version of Flutter. '
          'See https://flutter.dev/docs/development/tools/sdk/releases '
          'for previous releases of Flutter.');
865 866 867 868
      globals.printStatus('');
    }
  }

869 870
  void _registerSignalHandlers(String commandPath, DateTime startTime) {
    final SignalHandler handler = (io.ProcessSignal s) {
871
      Cache.releaseLock();
872 873 874 875
      _sendPostUsage(
        commandPath,
        const FlutterCommandResult(ExitStatus.killed),
        startTime,
876
        globals.systemClock.now(),
877 878
      );
    };
879 880
    globals.signals.addHandler(io.ProcessSignal.SIGTERM, handler);
    globals.signals.addHandler(io.ProcessSignal.SIGINT, handler);
881 882
  }

883 884 885 886
  /// 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.
887 888 889 890 891 892
  void _sendPostUsage(
    String commandPath,
    FlutterCommandResult commandResult,
    DateTime startTime,
    DateTime endTime,
  ) {
893 894 895
    if (commandPath == null) {
      return;
    }
896
    assert(commandResult != null);
897
    // Send command result.
898
    CommandResultEvent(commandPath, commandResult).send();
899 900

    // Send timing.
901
    final List<String> labels = <String>[
902
      if (commandResult.exitStatus != null)
903
        getEnumName(commandResult.exitStatus),
904
      if (commandResult.timingLabelParts?.isNotEmpty ?? false)
905 906
        ...commandResult.timingLabelParts,
    ];
907 908

    final String label = labels
909
        .where((String label) => !_isBlank(label))
910
        .join('-');
911
    globals.flutterUsage.sendTiming(
912 913 914 915
      'flutter',
      name,
      // If the command provides its own end time, use it. Otherwise report
      // the duration of the entire execution.
916
      (commandResult.endTimeOverride ?? endTime).difference(startTime),
917 918 919 920 921 922
      // 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,
    );
  }

923 924 925 926
  List<String> get _enabledExperiments => argParser.options.containsKey(FlutterOptions.kEnableExperiment)
    ? stringsArg(FlutterOptions.kEnableExperiment)
    : <String>[];

927 928 929 930 931 932 933 934
  /// 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
935
  Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
936 937
    // 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.
938
    if (shouldUpdateCache) {
939
      // First always update universal artifacts, as some of these (e.g.
940
      // ios-deploy on macOS) are required to determine `requiredArtifacts`.
941
      await globals.cache.updateAll(<DevelopmentArtifact>{DevelopmentArtifact.universal});
942

943
      await globals.cache.updateAll(await requiredArtifacts);
944
    }
945

946 947
    await validateCommand();

948
    if (shouldRunPub) {
949
      final FlutterProject project = FlutterProject.current();
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
      final Environment environment = Environment(
        artifacts: globals.artifacts,
        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,
        projectDir: project.directory,
      );

      await generateLocalizationsSyntheticPackage(
        environment: environment,
        buildSystem: globals.buildSystem,
      );

967 968 969 970
      await pub.get(
        context: PubContext.getVerifyContext(name),
        generateSyntheticPackage: project.manifest.generateSyntheticPackage,
      );
971 972
      // All done updating dependencies. Release the cache lock.
      Cache.releaseLock();
973
      await project.ensureReadyForPlatformSpecificTooling(checkProjects: true);
974 975
    } else {
      Cache.releaseLock();
976
    }
977

978
    setupApplicationPackages();
Devon Carew's avatar
Devon Carew committed
979

980
    if (commandPath != null) {
981 982
      final Map<CustomDimensions, Object> additionalUsageValues =
        <CustomDimensions, Object>{
983
          ...?await usageValues,
984 985
          CustomDimensions.commandHasTerminal: globals.stdio.hasTerminal,
          CustomDimensions.nullSafety: _enabledExperiments.contains('non-nullable'),
986 987
        };
      Usage.command(commandPath, parameters: additionalUsageValues);
988 989
    }

990
    return await runCommand();
991 992
  }

993 994
  /// The set of development artifacts required for this command.
  ///
995 996 997
  /// Defaults to an empty set. Including [DevelopmentArtifact.universal] is
  /// not required as it is always updated.
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
998

999
  /// Subclasses must implement this to execute the command.
1000
  /// Optionally provide a [FlutterCommandResult] to send more details about the
1001 1002
  /// execution for analytics.
  Future<FlutterCommandResult> runCommand();
1003

1004
  /// Find and return all target [Device]s based upon currently connected
1005
  /// devices and criteria entered by the user on the command line.
1006
  /// If no device can be found that meets specified criteria,
1007
  /// then print an error message and return null.
1008
  Future<List<Device>> findAllTargetDevices() async {
1009
    if (!globals.doctor.canLaunchAnything) {
1010
      globals.printError(userMessages.flutterNoDevelopmentDevice);
1011 1012
      return null;
    }
1013
    final DeviceManager deviceManager = globals.deviceManager;
1014
    List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current(), timeout: deviceDiscoveryTimeout);
1015 1016

    if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
1017
      globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId));
1018 1019
      return null;
    } else if (devices.isEmpty) {
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
      if (deviceManager.hasSpecifiedAllDevices) {
        globals.printStatus(userMessages.flutterNoDevicesFound);
      } else {
        globals.printStatus(userMessages.flutterNoSupportedDevices);
      }
      final List<Device> unsupportedDevices = await deviceManager.getDevices();
      if (unsupportedDevices.isNotEmpty) {
        final StringBuffer result = StringBuffer();
        result.writeln(userMessages.flutterFoundButUnsupportedDevices);
        result.writeAll(
          await Device.descriptions(unsupportedDevices)
              .map((String desc) => desc)
              .toList(),
          '\n',
        );
        result.writeln('');
        result.writeln(userMessages.flutterMissPlatformProjects(
          Device.devicesPlatformTypes(unsupportedDevices),
        ));
        globals.printStatus(result.toString());
      }
1041
      return null;
1042
    } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
1043
      if (deviceManager.hasSpecifiedDeviceId) {
1044
       globals.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId));
1045
      } else {
1046
        globals.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
1047
        devices = await deviceManager.getAllConnectedDevices();
1048
      }
1049
      globals.printStatus('');
1050
      await Device.printDevices(devices);
1051 1052
      return null;
    }
1053 1054 1055 1056 1057 1058
    return devices;
  }

  /// 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,
1059
  /// then print an error message and return null.
1060 1061
  Future<Device> findTargetDevice() async {
    List<Device> deviceList = await findAllTargetDevices();
1062
    if (deviceList == null) {
1063
      return null;
1064
    }
1065
    if (deviceList.length > 1) {
1066
      globals.printStatus(userMessages.flutterSpecifyDevice);
1067
      deviceList = await globals.deviceManager.getAllConnectedDevices();
1068
      globals.printStatus('');
1069 1070 1071 1072
      await Device.printDevices(deviceList);
      return null;
    }
    return deviceList.single;
1073 1074
  }

1075 1076
  @protected
  @mustCallSuper
1077
  Future<void> validateCommand() async {
1078
    if (_requiresPubspecYaml && !isUsingCustomPackagesPath) {
1079
      // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093

      // If there is no pubspec in the current directory, look in the parent
      // until one can be found.
      bool changedDirectory = false;
      while (!globals.fs.isFileSync('pubspec.yaml')) {
        final Directory nextCurrent = globals.fs.currentDirectory.parent;
        if (nextCurrent == null || nextCurrent.path == globals.fs.currentDirectory.path) {
          throw ToolExit(userMessages.flutterNoPubspec);
        }
        globals.fs.currentDirectory = nextCurrent;
        changedDirectory = true;
      }
      if (changedDirectory) {
        globals.printStatus('Changing current working directory to: ${globals.fs.currentDirectory.path}');
1094
      }
1095
    }
1096 1097

    if (_usesTargetOption) {
1098
      final String targetPath = targetFile;
1099
      if (!globals.fs.isFileSync(targetPath)) {
1100
        throw ToolExit(userMessages.flutterTargetFileMissing(targetPath));
1101
      }
1102 1103
    }
  }
1104

1105 1106 1107 1108 1109 1110 1111
  @override
  String get usage {
    final String usageWithoutDescription = super.usage.substring(
      // The description plus two newlines.
      description.length + 2,
    );
    final String help = <String>[
1112 1113
      if (deprecated)
        '$warningMark Deprecated. This command will be removed in a future version of Flutter.',
1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
      description,
      '',
      'Global options:',
      runner.argParser.usage,
      '',
      usageWithoutDescription,
    ].join('\n');
    return help;
  }

1124
  ApplicationPackageStore applicationPackages;
1125 1126 1127 1128 1129 1130 1131 1132 1133

  /// Gets the parsed command-line option named [name] as `bool`.
  bool boolArg(String name) => argResults[name] as bool;

  /// Gets the parsed command-line option named [name] as `String`.
  String stringArg(String name) => argResults[name] as String;

  /// Gets the parsed command-line option named [name] as `List<String>`.
  List<String> stringsArg(String name) => argResults[name] as List<String>;
1134
}
1135

1136 1137 1138 1139 1140 1141 1142 1143
/// A mixin which applies an implementation of [requiredArtifacts] that only
/// downloads artifacts corresponding to an attached device.
mixin DeviceBasedDevelopmentArtifacts on FlutterCommand {
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
    // If there are no attached devices, use the default configuration.
    // Otherwise, only add development artifacts which correspond to a
    // connected device.
1144
    final List<Device> devices = await globals.deviceManager.getDevices();
1145 1146 1147 1148 1149 1150
    if (devices.isEmpty) {
      return super.requiredArtifacts;
    }
    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
      DevelopmentArtifact.universal,
    };
1151
    for (final Device device in devices) {
1152
      final TargetPlatform targetPlatform = await device.targetPlatform;
1153 1154 1155
      final DevelopmentArtifact developmentArtifact = _artifactFromTargetPlatform(targetPlatform);
      if (developmentArtifact != null) {
        artifacts.add(developmentArtifact);
1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
      }
    }
    return artifacts;
  }
}

/// A mixin which applies an implementation of [requiredArtifacts] that only
/// downloads artifacts corresponding to a target device.
mixin TargetPlatformBasedDevelopmentArtifacts on FlutterCommand {
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
    // If there is no specified target device, fallback to the default
    // confiugration.
1169
    final String rawTargetPlatform = stringArg('target-platform');
1170 1171 1172 1173 1174
    final TargetPlatform targetPlatform = getTargetPlatformForName(rawTargetPlatform);
    if (targetPlatform == null) {
      return super.requiredArtifacts;
    }

1175
    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{};
1176 1177 1178
    final DevelopmentArtifact developmentArtifact = _artifactFromTargetPlatform(targetPlatform);
    if (developmentArtifact != null) {
      artifacts.add(developmentArtifact);
1179 1180 1181 1182 1183
    }
    return artifacts;
  }
}

1184 1185 1186 1187
// Returns the development artifact for the target platform, or null
// if none is supported
DevelopmentArtifact _artifactFromTargetPlatform(TargetPlatform targetPlatform) {
  switch (targetPlatform) {
1188
    case TargetPlatform.android:
1189 1190 1191 1192
    case TargetPlatform.android_arm:
    case TargetPlatform.android_arm64:
    case TargetPlatform.android_x64:
    case TargetPlatform.android_x86:
1193
      return DevelopmentArtifact.androidGenSnapshot;
1194
    case TargetPlatform.web_javascript:
1195 1196 1197
      return DevelopmentArtifact.web;
    case TargetPlatform.ios:
      return DevelopmentArtifact.iOS;
1198
    case TargetPlatform.darwin_x64:
1199
      if (featureFlags.isMacOSEnabled) {
1200 1201 1202
        return DevelopmentArtifact.macOS;
      }
      return null;
1203
    case TargetPlatform.windows_x64:
1204
      if (featureFlags.isWindowsEnabled) {
1205 1206 1207
        return DevelopmentArtifact.windows;
      }
      return null;
1208
    case TargetPlatform.linux_x64:
1209
      if (featureFlags.isLinuxEnabled) {
1210 1211 1212
        return DevelopmentArtifact.linux;
      }
      return null;
1213 1214
    case TargetPlatform.fuchsia_arm64:
    case TargetPlatform.fuchsia_x64:
1215 1216 1217 1218 1219 1220
    case TargetPlatform.tester:
      // No artifacts currently supported.
      return null;
  }
  return null;
}
1221 1222 1223

/// 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 == null || s.trim().isEmpty;