flutter_command.dart 60.2 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 11

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

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

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

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

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

66 67
  final ExitStatus exitStatus;

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

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

  @override
  String toString() {
    switch (exitStatus) {
      case ExitStatus.success:
        return 'success';
      case ExitStatus.warning:
        return 'warning';
      case ExitStatus.fail:
        return 'fail';
90 91
      case ExitStatus.killed:
        return 'killed';
92 93
    }
  }
94 95
}

96 97 98 99
/// Common flutter command line options.
class FlutterOptions {
  static const String kExtraFrontEndOptions = 'extra-front-end-options';
  static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options';
100
  static const String kEnableExperiment = 'enable-experiment';
101 102
  static const String kFileSystemRoot = 'filesystem-root';
  static const String kFileSystemScheme = 'filesystem-scheme';
103
  static const String kSplitDebugInfoOption = 'split-debug-info';
104
  static const String kDartObfuscationOption = 'obfuscate';
105
  static const String kDartDefinesOption = 'dart-define';
106
  static const String kBundleSkSLPathOption = 'bundle-sksl-path';
107
  static const String kPerformanceMeasurementFile = 'performance-measurement-file';
108
  static const String kNullSafety = 'sound-null-safety';
109
  static const String kDeviceUser = 'device-user';
110
  static const String kDeviceTimeout = 'device-timeout';
111
  static const String kAnalyzeSize = 'analyze-size';
112
  static const String kCodeSizeDirectory = 'code-size-directory';
113
  static const String kNullAssertions = 'null-assertions';
114
  static const String kAndroidGradleDaemon = 'android-gradle-daemon';
115
  static const String kDeferredComponents = 'deferred-components';
116
  static const String kAndroidProjectArgs = 'android-project-arg';
117
  static const String kInitializeFromDill = 'initialize-from-dill';
118
  static const String kFatalWarnings = 'fatal-warnings';
119 120
}

121 122 123 124 125 126 127
/// flutter command categories for usage.
class FlutterCommandCategory {
  static const String sdk = 'Flutter SDK';
  static const String project = 'Project';
  static const String tools = 'Tools & Devices';
}

128
abstract class FlutterCommand extends Command<void> {
129 130 131
  /// The currently executing command (or sub-command).
  ///
  /// Will be `null` until the top-most command has begun execution.
132
  static FlutterCommand? get current => context.get<FlutterCommand>();
133

134 135 136
  /// The option name for a custom observatory port.
  static const String observatoryPortOption = 'observatory-port';

137 138 139
  /// The option name for a custom DevTools server address.
  static const String kDevToolsServerAddress = 'devtools-server-address';

140 141 142
  /// The flag name for whether to launch the DevTools or not.
  static const String kEnableDevTools = 'devtools';

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

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
  /// The map used to convert web-renderer option to a List of dart-defines.
  static const Map<String, Iterable<String>> _webRendererDartDefines =
  <String, Iterable<String>> {
    'auto': <String>[
      'FLUTTER_WEB_AUTO_DETECT=true',
    ],
    'canvaskit': <String>[
      'FLUTTER_WEB_AUTO_DETECT=false',
      'FLUTTER_WEB_USE_SKIA=true'
    ],
    'html': <String>[
      'FLUTTER_WEB_AUTO_DETECT=false',
      'FLUTTER_WEB_USE_SKIA=false'
    ],
  };

162 163
  @override
  ArgParser get argParser => _argParser;
164
  final ArgParser _argParser = ArgParser(
165
    usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
166
  );
167

168
  @override
169
  FlutterCommandRunner? get runner => super.runner as FlutterCommandRunner?;
170

171 172
  bool _requiresPubspecYaml = false;

173 174 175 176
  /// Whether this command uses the 'target' option.
  bool _usesTargetOption = false;

  bool _usesPubOption = false;
177

178 179 180 181
  bool _usesPortOption = false;

  bool _usesIpv6Flag = false;

182 183
  bool _usesFatalWarnings = false;

184 185
  DeprecationBehavior get deprecationBehavior => DeprecationBehavior.none;

186
  bool get shouldRunPub => _usesPubOption && boolArg('pub');
187

188 189
  bool get shouldUpdateCache => true;

190 191 192 193 194
  bool get deprecated => false;

  @override
  bool get hidden => deprecated;

195
  bool _excludeDebug = false;
196
  bool _excludeRelease = false;
197

198 199 200 201
  void requiresPubspecYaml() {
    _requiresPubspecYaml = true;
  }

202
  void usesWebOptions({ required bool verboseHelp }) {
203
    argParser.addOption('web-hostname',
204
      defaultsTo: 'localhost',
205 206 207 208 209
      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.',
210
      hide: !verboseHelp,
211
    );
212
    argParser.addOption('web-port',
213 214
      help: 'The host port to serve the web application from. If not provided, the tool '
        'will select a random open port on the host.',
215
      hide: !verboseHelp,
216
    );
217 218
    argParser.addOption('web-server-debug-protocol',
      allowed: <String>['sse', 'ws'],
219
      defaultsTo: 'ws',
220
      help: 'The protocol (SSE or WebSockets) to use for the debug service proxy '
221
      'when using the Web Server device and Dart Debug extension. '
222 223
      'This is useful for editors/debug adapters that do not support debugging '
      'over SSE (the default protocol for Web Server/Dart Debugger extension).',
224
      hide: !verboseHelp,
225
    );
226 227
    argParser.addOption('web-server-debug-backend-protocol',
      allowed: <String>['sse', 'ws'],
228
      defaultsTo: 'ws',
229 230 231 232
      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.',
233
      hide: !verboseHelp,
234
    );
235 236
    argParser.addOption('web-server-debug-injected-client-protocol',
      allowed: <String>['sse', 'ws'],
237
      defaultsTo: 'ws',
238 239 240 241 242 243
      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,
    );
244 245 246
    argParser.addFlag('web-allow-expose-url',
      help: 'Enables daemon-to-editor requests (app.exposeUrl) for exposing URLs '
        'when running on remote machines.',
247
      hide: !verboseHelp,
248
    );
249 250 251
    argParser.addFlag('web-run-headless',
      help: 'Launches the browser in headless mode. Currently only Chrome '
        'supports this option.',
252
      hide: !verboseHelp,
253 254 255 256 257 258
    );
    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/).',
259
      hide: !verboseHelp,
260
    );
261
    argParser.addFlag('web-enable-expression-evaluation',
262
      defaultsTo: true,
263
      help: 'Enables expression evaluation in the debugger.',
264
      hide: !verboseHelp,
265
    );
266 267
  }

268 269 270
  void usesTargetOption() {
    argParser.addOption('target',
      abbr: 't',
271
      defaultsTo: bundle.defaultMainPath,
272
      help: 'The main entry-point file of the application, as run on the device.\n'
273
            'If the "--target" option is omitted, but a file name is provided on '
274 275
            'the command line, then that is used instead.',
      valueHelp: 'path');
276 277 278
    _usesTargetOption = true;
  }

279 280 281 282 283 284 285 286 287
  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;
  }

288
  String get targetFile {
289 290
    if (argResults?.wasParsed('target') == true) {
      return stringArg('target')!;
291
    }
292 293 294
    final List<String>? rest = argResults?.rest;
    if (rest != null && rest.isNotEmpty) {
      return rest.first;
295 296
    }
    return bundle.defaultMainPath;
297 298
  }

299 300 301
  /// Path to the Dart's package config file.
  ///
  /// This can be overridden by some of its subclasses.
302
  String? get packagesPath => globalResults?['packages'] as String?;
303

304 305 306
  /// The value of the `--filesystem-scheme` argument.
  ///
  /// This can be overridden by some of its subclasses.
307
  String? get fileSystemScheme =>
308 309 310 311 312 313 314
    argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
          ? stringArg(FlutterOptions.kFileSystemScheme)
          : null;

  /// The values of the `--filesystem-root` argument.
  ///
  /// This can be overridden by some of its subclasses.
315
  List<String>? get fileSystemRoots =>
316 317 318 319
    argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
          ? stringsArg(FlutterOptions.kFileSystemRoot)
          : null;

320
  void usesPubOption({bool hide = false}) {
321 322
    argParser.addFlag('pub',
      defaultsTo: true,
323
      hide: hide,
324
      help: 'Whether to run "flutter pub get" before executing this command.');
325 326 327
    _usesPubOption = true;
  }

328 329
  /// Adds flags for using a specific filesystem root and scheme.
  ///
330 331
  /// The `hide` argument indicates whether or not to hide these options when
  /// the user asks for help.
332
  void usesFilesystemOptions({ required bool hide }) {
333 334 335 336 337 338 339
    argParser
      ..addOption('output-dill',
        hide: hide,
        help: 'Specify the path to frontend server output kernel file.',
      )
      ..addMultiOption(FlutterOptions.kFileSystemRoot,
        hide: hide,
340 341 342 343
        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.',
344 345 346 347
      )
      ..addOption(FlutterOptions.kFileSystemScheme,
        defaultsTo: 'org-dartlang-root',
        hide: hide,
348 349
        help: 'Specify the scheme that is used for virtual file system used in '
              'compilation. See also the "--${FlutterOptions.kFileSystemRoot}" option.',
350 351 352
      );
  }

353
  /// Adds options for connecting to the Dart VM observatory port.
354
  void usesPortOptions({ required bool verboseHelp }) {
355
    argParser.addOption(observatoryPortOption,
356
        help: '(deprecated; use host-vmservice-port instead) '
357
              'Listen to the given port for an observatory debugger connection.\n'
358
              'Specifying port 0 (the default) will find a random free port.\n '
359 360
              'if the Dart Development Service (DDS) is enabled, this will not be the port '
              'of the Observatory instance advertised on the command line.',
361
        hide: !verboseHelp,
362
    );
363 364 365 366 367 368 369 370 371 372
    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.'
    );
373 374 375
    _usesPortOption = true;
  }

376
  void addDevToolsOptions({required bool verboseHelp}) {
377 378 379 380
    argParser.addFlag(
      kEnableDevTools,
      hide: !verboseHelp,
      defaultsTo: true,
381
      help: 'Enable (or disable, with "--no-$kEnableDevTools") the launching of the '
382
            'Flutter DevTools debugger and profiler. '
383
            'If specified, "--$kDevToolsServerAddress" is ignored.'
384 385 386 387
    );
    argParser.addOption(
      kDevToolsServerAddress,
      hide: !verboseHelp,
388
      help: 'When this value is provided, the Flutter tool will not spin up a '
389
            'new DevTools server instance, and will instead use the one provided '
390
            'at the given address. Ignored if "--no-$kEnableDevTools" is specified.'
391
    );
392 393
  }

394
  void addDdsOptions({required bool verboseHelp}) {
395 396
    argParser.addOption('dds-port',
      help: 'When this value is provided, the Dart Development Service (DDS) will be '
397 398 399
            'bound to the provided port.\n'
            'Specifying port 0 (the default) will find a random free port.'
    );
400
    argParser.addFlag(
401
      'dds',
402
      hide: !verboseHelp,
403 404 405 406 407 408 409 410 411 412 413 414 415
      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).'
416 417 418
    );
  }

419 420 421 422 423 424 425 426 427 428 429
  late final bool enableDds = () {
    bool ddsEnabled = false;
    if (argResults?.wasParsed('disable-dds') == true) {
      if (argResults?.wasParsed('dds') == true) {
        throwToolExit(
            'The "--[no-]dds" and "--[no-]disable-dds" arguments are mutually exclusive. Only specify "--[no-]dds".');
      }
      ddsEnabled = !boolArg('disable-dds');
      // TODO(ianh): enable the following code once google3 is migrated away from --disable-dds (and add test to flutter_command_test.dart)
      if (false) { // ignore: dead_code
        if (ddsEnabled) {
430
          globals.printWarning('${globals.logger.terminal
431 432
              .warningMark} The "--no-disable-dds" argument is deprecated and redundant, and should be omitted.');
        } else {
433
          globals.printWarning('${globals.logger.terminal
434
              .warningMark} The "--disable-dds" argument is deprecated. Use "--no-dds" instead.');
435 436
        }
      }
437 438
    } else {
      ddsEnabled = boolArg('dds');
439
    }
440 441
    return ddsEnabled;
  }();
442

443 444
  bool get _hostVmServicePortProvided => argResults?.wasParsed('observatory-port') == true ||
                                         argResults?.wasParsed('host-vmservice-port') == true;
445 446

  int _tryParseHostVmservicePort() {
447 448 449 450 451
    final String? observatoryPort = stringArg('observatory-port');
    final String? hostPort = stringArg('host-vmservice-port');
    if (observatoryPort == null && hostPort == null) {
      throwToolExit('Invalid port for `--observatory-port/--host-vmservice-port`');
    }
452
    try {
453
      return int.parse((observatoryPort ?? hostPort)!);
454 455 456 457 458
    } on FormatException catch (error) {
      throwToolExit('Invalid port for `--observatory-port/--host-vmservice-port`: $error');
    }
  }

459
  int get ddsPort {
460
    if (argResults?.wasParsed('dds-port') != true && _hostVmServicePortProvided) {
461 462
      // If an explicit DDS port is _not_ provided, use the host-vmservice-port for DDS.
      return _tryParseHostVmservicePort();
463
    } else if (argResults?.wasParsed('dds-port') == true) {
464
      // If an explicit DDS port is provided, use dds-port for DDS.
465
      return int.tryParse(stringArg('dds-port')!) ?? 0;
466
    }
467
    // Otherwise, DDS can bind to a random port.
468 469 470
    return 0;
  }

471 472 473
  Uri? get devToolsServerAddress {
    if (argResults?.wasParsed(kDevToolsServerAddress) == true) {
      final Uri? uri = Uri.tryParse(stringArg(kDevToolsServerAddress)!);
474 475 476 477 478 479 480
      if (uri != null && uri.host.isNotEmpty && uri.port != 0) {
        return uri;
      }
    }
    return null;
  }

481 482 483 484 485 486 487
  /// 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.
488
  int? get hostVmservicePort {
489
    if (!_usesPortOption || !_hostVmServicePortProvided) {
490 491
      return null;
    }
492 493
    if (argResults?.wasParsed('observatory-port') == true &&
        argResults?.wasParsed('host-vmservice-port') == true) {
494 495 496
      throwToolExit('Only one of "--observatory-port" and '
        '"--host-vmservice-port" may be specified.');
    }
497 498 499
    // 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.
500
    if (enableDds && argResults?.wasParsed('dds-port') != true) {
501
      return null;
502
    }
503
    return _tryParseHostVmservicePort();
504 505 506
  }

  /// Gets the vmservice port provided to in the 'device-vmservice-port' option.
507 508
  ///
  /// If no port is set, returns null.
509 510 511
  int? get deviceVmservicePort {
    final String? devicePort = stringArg('device-vmservice-port');
    if (!_usesPortOption || devicePort == null) {
512 513 514
      return null;
    }
    try {
515
      return int.parse(devicePort);
516 517
    } on FormatException catch (error) {
      throwToolExit('Invalid port for `--device-vmservice-port`: $error');
518 519 520
    }
  }

521 522
  void addPublishPort({ bool enabledByDefault = true, bool verboseHelp = false }) {
    argParser.addFlag('publish-port',
523 524 525 526 527
      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,
    );
528 529 530 531
  }

  bool get disablePortPublication => !boolArg('publish-port');

532
  void usesIpv6Flag({required bool verboseHelp}) {
533 534 535 536
    argParser.addFlag(ipv6Flag,
      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 '
537 538
            '"--debug-port" flag is not set.',
      hide: !verboseHelp,
539 540 541 542
    );
    _usesIpv6Flag = true;
  }

543
  bool? get ipv6 => _usesIpv6Flag ? boolArg('ipv6') : null;
544

545 546
  void usesBuildNumberOption() {
    argParser.addOption('build-number',
547 548
        help: 'An identifier used as an internal version number.\n'
              'Each build must have a unique identifier to differentiate it from previous builds.\n'
549
              'It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.\n'
550 551
              'On Android it is used as "versionCode".\n'
              'On Xcode builds it is used as "CFBundleVersion".',
552
    );
553 554 555 556 557 558
  }

  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'
559 560
              'On Android it is used as "versionName".\n'
              'On Xcode builds it is used as "CFBundleShortVersionString".',
561 562 563
        valueHelp: 'x.y.z');
  }

564
  void usesDartDefineOption() {
565
    argParser.addMultiOption(
566 567
      FlutterOptions.kDartDefinesOption,
      aliases: <String>[ kDartDefines ], // supported for historical reasons
568 569 570
      help: 'Additional key-value pairs that will be available as constants '
            'from the String.fromEnvironment, bool.fromEnvironment, int.fromEnvironment, '
            'and double.fromEnvironment constructors.\n'
571
            'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefinesOption}" multiple times.',
572
      valueHelp: 'foo=bar',
573
      splitCommas: false,
574 575 576
    );
  }

577 578
  void usesWebRendererOption() {
    argParser.addOption('web-renderer',
579
      defaultsTo: 'auto',
580
      allowed: <String>['auto', 'canvaskit', 'html'],
581 582
      help: 'The renderer implementation to use when building for the web.',
      allowedHelp: <String, String>{
583
        'html': 'Always use the HTML renderer. This renderer uses a combination of HTML, CSS, SVG, 2D Canvas, and WebGL.',
584 585 586
        'canvaskit': 'Always use the CanvasKit renderer. This renderer uses WebGL and WebAssembly to render graphics.',
        'auto': 'Use the HTML renderer on mobile devices, and CanvasKit on desktop devices.',
      }
587 588 589
    );
  }

590 591 592 593 594 595
  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');
  }

596 597 598 599 600 601 602 603
  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'
    );
  }

604 605 606
  /// Whether it is safe for this command to use a cached pub invocation.
  bool get cachePubGet => true;

607 608 609
  /// Whether this command should report null safety analytics.
  bool get reportNullSafety => false;

610 611 612 613
  late final Duration? deviceDiscoveryTimeout = () {
    if (argResults?.options.contains(FlutterOptions.kDeviceTimeout) == true
        && argResults?.wasParsed(FlutterOptions.kDeviceTimeout) == true) {
      final int? timeoutSeconds = int.tryParse(stringArg(FlutterOptions.kDeviceTimeout)!);
614
      if (timeoutSeconds == null) {
615
        throwToolExit( 'Could not parse "--${FlutterOptions.kDeviceTimeout}" argument. It must be an integer.');
616
      }
617
      return Duration(seconds: timeoutSeconds);
618
    }
619 620
    return null;
  }();
621

622
  void addBuildModeFlags({
623
    required bool verboseHelp,
624 625 626 627
    bool defaultToRelease = true,
    bool excludeDebug = false,
    bool excludeRelease = false,
  }) {
628 629 630
    // A release build must be the default if a debug build is not possible.
    assert(defaultToRelease || !excludeDebug);
    _excludeDebug = excludeDebug;
631
    _excludeRelease = excludeRelease;
632
    defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug;
633

634 635 636 637 638
    if (!excludeDebug) {
      argParser.addFlag('debug',
        negatable: false,
        help: 'Build a debug version of your app${defaultToRelease ? '' : ' (default mode)'}.');
    }
639 640
    argParser.addFlag('profile',
      negatable: false,
641
      help: 'Build a version of your app specialized for performance profiling.');
642 643 644 645 646 647 648 649 650
    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)' : ''}.');
    }
651 652
  }

653 654 655
  void addSplitDebugInfoOption() {
    argParser.addOption(FlutterOptions.kSplitDebugInfoOption,
      help: 'In a release build, this flag reduces application size by storing '
656 657 658 659 660 661 662
            '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}".',
663
      valueHelp: 'v1.2.3/',
664 665 666
    );
  }

667 668 669
  void addDartObfuscationOption() {
    argParser.addFlag(FlutterOptions.kDartObfuscationOption,
      help: 'In a release build, this flag removes identifiers and replaces them '
670 671 672 673 674 675 676 677 678 679 680 681
            '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.'
682 683 684
    );
  }

685
  void addBundleSkSLPathOption({ required bool hide }) {
686 687 688 689 690
    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,
691
      valueHelp: 'flutter_1.sksl'
692 693 694
    );
  }

695
  void addTreeShakeIconsFlag({
696
    bool? enabledByDefault
697
  }) {
698
    argParser.addFlag('tree-shake-icons',
699 700
      defaultsTo: enabledByDefault
        ?? kIconTreeShakerEnabledDefault,
701 702 703 704
      help: 'Tree shake icon fonts so that only glyphs used by the application remain.',
    );
  }

705
  void addShrinkingFlag({ required bool verboseHelp }) {
Emmanuel Garcia's avatar
Emmanuel Garcia committed
706
    argParser.addFlag('shrink',
707 708
      hide: !verboseHelp,
      help: 'This flag has no effect. Code shrinking is always enabled in release builds. '
709 710
            'To learn more, see: https://developer.android.com/studio/build/shrink-code'
    );
Emmanuel Garcia's avatar
Emmanuel Garcia committed
711 712
  }

713
  void addNullSafetyModeOptions({ required bool hide }) {
714
    argParser.addFlag(FlutterOptions.kNullSafety,
715 716 717
      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, '
718 719 720
        '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.',
721
      defaultsTo: true,
722
      hide: hide,
723
    );
724 725 726
    argParser.addFlag(FlutterOptions.kNullAssertions,
      help:
        'Perform additional null assertions on the boundaries of migrated and '
727
        'un-migrated code. This setting is not currently supported on desktop '
728 729
        'devices.'
    );
730 731
  }

732 733
  /// Enables support for the hidden options --extra-front-end-options and
  /// --extra-gen-snapshot-options.
734
  void usesExtraDartFlagOptions({ required bool verboseHelp }) {
735 736
    argParser.addMultiOption(FlutterOptions.kExtraFrontEndOptions,
      aliases: <String>[ kExtraFrontEndOptions ], // supported for historical reasons
737 738 739 740
      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,
741
    );
742 743
    argParser.addMultiOption(FlutterOptions.kExtraGenSnapshotOptions,
      aliases: <String>[ kExtraGenSnapshotOptions ], // supported for historical reasons
744
      help: 'A comma-separated list of additional command line arguments that will be passed directly to the Dart native compiler. '
745
            '(Only used in "--profile" or "--release" builds.) '
746 747 748
            'For example, "--${FlutterOptions.kExtraGenSnapshotOptions}=--no-strip".',
      valueHelp: '--foo,--bar',
      hide: !verboseHelp,
749
    );
750 751
  }

752
  void usesFuchsiaOptions({ bool hide = false }) {
753 754
    argParser.addOption(
      'target-model',
755
      help: 'Target model that determines what core libraries are available.',
756 757 758 759 760 761 762 763
      defaultsTo: 'flutter',
      hide: hide,
      allowed: const <String>['flutter', 'flutter_runner'],
    );
    argParser.addOption(
      'module',
      abbr: 'm',
      hide: hide,
764
      help: 'The name of the module (required if attaching to a fuchsia device).',
765 766 767 768
      valueHelp: 'module-name',
    );
  }

769
  void addEnableExperimentation({ required bool hide }) {
770 771 772
    argParser.addMultiOption(
      FlutterOptions.kEnableExperiment,
      help:
773
        'The name of an experimental Dart feature to enable. For more information see: '
774
        'https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md',
775
      hide: hide,
776 777 778
    );
  }

779 780 781 782 783
  void addBuildPerformanceFile({ bool hide = false }) {
    argParser.addOption(
      FlutterOptions.kPerformanceMeasurementFile,
      help:
        'The name of a file where flutter assemble performance and '
784 785
        'cached-ness information will be written in a JSON format.',
      hide: hide,
786 787 788
    );
  }

789 790 791 792
  void addAndroidSpecificBuildOptions({ bool hide = false }) {
    argParser.addFlag(
      FlutterOptions.kAndroidGradleDaemon,
      help: 'Whether to enable the Gradle daemon when performing an Android build. '
793 794 795 796
            '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.',
797
      defaultsTo: true,
798
      hide: hide,
799
    );
800 801 802
    argParser.addMultiOption(
      FlutterOptions.kAndroidProjectArgs,
      help: 'Additional arguments specified as key=value that are passed directly to the gradle '
803
            'project via the -P flag. These can be accessed in build.gradle via the "project.property" API.',
804 805 806
      splitCommas: false,
      abbr: 'P',
    );
807 808
  }

809 810 811 812 813 814 815 816
  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 '
817 818
        'dart:html or the other dart web libraries, please file a bug at: '
        'https://github.com/dart-lang/sdk/issues/labels/web-libraries'
819 820 821
    );
  }

822
  void usesInitializeFromDillOption({ required bool hide }) {
823 824 825 826 827 828 829
    argParser.addOption(FlutterOptions.kInitializeFromDill,
      help: 'Initializes the resident compiler with a specific kernel file instead of '
        'the default cached location.',
      hide: hide,
    );
  }

830 831 832 833 834 835 836 837 838
  void addMultidexOption({ bool hide = false }) {
    argParser.addFlag('multidex',
      defaultsTo: true,
      help: 'When enabled, indicates that the app should be built with multidex support. This '
            'flag adds the dependencies for multidex when the minimum android sdk is 20 or '
            'below. For android sdk versions 21 and above, multidex support is native.',
    );
  }

839 840 841 842 843 844 845 846 847
  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.',
    );
  }

848
  /// Adds build options common to all of the desktop build commands.
849
  void addCommonDesktopBuildOptions({ required bool verboseHelp }) {
850 851 852 853 854 855 856 857 858 859
    addBuildModeFlags(verboseHelp: verboseHelp);
    addBuildPerformanceFile(hide: !verboseHelp);
    addBundleSkSLPathOption(hide: !verboseHelp);
    addDartObfuscationOption();
    addEnableExperimentation(hide: !verboseHelp);
    addNullSafetyModeOptions(hide: !verboseHelp);
    addSplitDebugInfoOption();
    addTreeShakeIconsFlag();
    usesAnalyzeSizeFlag();
    usesDartDefineOption();
860
    usesExtraDartFlagOptions(verboseHelp: verboseHelp);
861 862 863 864 865
    usesPubOption();
    usesTargetOption();
    usesTrackWidgetCreation(verboseHelp: verboseHelp);
  }

866 867 868 869
  /// The build mode that this command will use if no build mode is
  /// explicitly specified.
  ///
  /// Use [getBuildMode] to obtain the actual effective build mode.
870
  BuildMode defaultBuildMode = BuildMode.debug;
871

872
  BuildMode getBuildMode() {
873 874
    // No debug when _excludeDebug is true.
    // If debug is not excluded, then take the command line flag.
875
    final bool debugResult = !_excludeDebug && boolArg('debug');
876 877
    final bool jitReleaseResult = !_excludeRelease && boolArg('jit-release');
    final bool releaseResult = !_excludeRelease && boolArg('release');
878 879
    final List<bool> modeFlags = <bool>[
      debugResult,
880
      jitReleaseResult,
881
      boolArg('profile'),
882
      releaseResult,
883
    ];
884
    if (modeFlags.where((bool flag) => flag).length > 1) {
885
      throw UsageException('Only one of "--debug", "--profile", "--jit-release", '
886
                           'or "--release" can be specified.', '');
887
    }
888
    if (debugResult) {
889
      return BuildMode.debug;
890
    }
891
    if (boolArg('profile')) {
892 893
      return BuildMode.profile;
    }
894
    if (releaseResult) {
895 896
      return BuildMode.release;
    }
897
    if (jitReleaseResult) {
898 899
      return BuildMode.jitRelease;
    }
900
    return defaultBuildMode;
901 902
  }

903 904 905 906
  void usesFlavorOption() {
    argParser.addOption(
      'flavor',
      help: 'Build a custom app flavor as defined by platform-specific build setup.\n'
907 908 909 910 911
            'Supports the use of product flavors in Android Gradle scripts, and '
            'the use of custom Xcode schemes.',
    );
  }

912
  void usesTrackWidgetCreation({ bool hasEffect = true, required bool verboseHelp }) {
913 914 915
    argParser.addFlag(
      'track-widget-creation',
      hide: !hasEffect && !verboseHelp,
916
      defaultsTo: true,
917 918
      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).',
919 920 921
    );
  }

922 923 924
  void usesAnalyzeSizeFlag() {
    argParser.addFlag(
      FlutterOptions.kAnalyzeSize,
925
      help: 'Whether to produce additional profile information for artifact output size. '
926 927 928
            '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'
929
            'By default, the intermediate output files will be placed in a transient directory in the '
930
            'build directory. This can be overridden with the "--${FlutterOptions.kCodeSizeDirectory}" option.\n'
931
            'This flag cannot be combined with "--${FlutterOptions.kSplitDebugInfoOption}".'
932
    );
933 934 935 936 937 938

    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.'
    );
939 940
  }

941
  /// Compute the [BuildInfo] for the current flutter command.
942 943
  /// Commands that build multiple build modes can pass in a [forcedBuildMode]
  /// to be used instead of parsing flags.
944 945
  ///
  /// Throws a [ToolExit] if the current set of options is not compatible with
946
  /// each other.
947
  Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async {
948 949
    final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation') &&
      boolArg('track-widget-creation');
950

951
    final String? buildNumber = argParser.options.containsKey('build-number')
952 953
      ? stringArg('build-number')
      : null;
954

955
    final File packagesFile = globals.fs.file(
956
      packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'));
957 958 959
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
        packagesFile, logger: globals.logger, throwOnError: false);

960 961
    final List<String> experiments =
      argParser.options.containsKey(FlutterOptions.kEnableExperiment)
962
        ? stringsArg(FlutterOptions.kEnableExperiment).toList()
963 964 965
        : <String>[];
    final List<String> extraGenSnapshotOptions =
      argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
966
        ? stringsArg(FlutterOptions.kExtraGenSnapshotOptions).toList()
967
        : <String>[];
968
    final List<String> extraFrontEndOptions =
969
      argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
970
          ? stringsArg(FlutterOptions.kExtraFrontEndOptions).toList()
971 972 973 974
          : <String>[];

    if (experiments.isNotEmpty) {
      for (final String expFlag in experiments) {
975
        final String flag = '--enable-experiment=$expFlag';
976
        extraFrontEndOptions.add(flag);
977
        extraGenSnapshotOptions.add(flag);
978 979 980
      }
    }

981
    String? codeSizeDirectory;
982
    if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) && boolArg(FlutterOptions.kAnalyzeSize)) {
983
      Directory directory = globals.fsUtils.getUniqueDirectory(
984 985 986
        globals.fs.directory(getBuildDirectory()),
        'flutter_size',
      );
987 988 989
      if (argParser.options.containsKey(FlutterOptions.kCodeSizeDirectory) && stringArg(FlutterOptions.kCodeSizeDirectory) != null) {
        directory = globals.fs.directory(stringArg(FlutterOptions.kCodeSizeDirectory));
      }
990 991
      directory.createSync(recursive: true);
      codeSizeDirectory = directory.path;
992 993
    }

994
    NullSafetyMode nullSafetyMode = NullSafetyMode.sound;
995 996
    if (argParser.options.containsKey(FlutterOptions.kNullSafety)) {
      // Explicitly check for `true` and `false` so that `null` results in not
997 998
      // passing a flag. Examine the entrypoint file to determine if it
      // is opted in or out.
999
      final bool wasNullSafetyFlagParsed = argResults?.wasParsed(FlutterOptions.kNullSafety) == true;
1000 1001
      if (!wasNullSafetyFlagParsed && (argParser.options.containsKey('target') || forcedTargetFile != null)) {
        final File entrypointFile = forcedTargetFile ?? globals.fs.file(targetFile);
1002 1003 1004
        final LanguageVersion languageVersion = determineLanguageVersion(
          entrypointFile,
          packageConfig.packageOf(entrypointFile.absolute.uri),
1005
          Cache.flutterRoot!,
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
        );
        // Extra frontend options are only provided if explicitly
        // requested.
        if (languageVersion.major >= nullSafeVersion.major && languageVersion.minor >= nullSafeVersion.minor) {
          nullSafetyMode = NullSafetyMode.sound;
        } else {
          nullSafetyMode = NullSafetyMode.unsound;
        }
      } else if (!wasNullSafetyFlagParsed) {
        // This mode is only used for commands which do not build a single target like
        // 'flutter test'.
1017 1018
        nullSafetyMode = NullSafetyMode.autodetect;
      } else if (boolArg(FlutterOptions.kNullSafety)) {
1019
        nullSafetyMode = NullSafetyMode.sound;
1020
        extraFrontEndOptions.add('--sound-null-safety');
1021
      } else {
1022
        nullSafetyMode = NullSafetyMode.unsound;
1023 1024 1025 1026
        extraFrontEndOptions.add('--no-sound-null-safety');
      }
    }

1027 1028 1029
    final bool dartObfuscation = argParser.options.containsKey(FlutterOptions.kDartObfuscationOption)
      && boolArg(FlutterOptions.kDartObfuscationOption);

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

1034 1035 1036
    final bool androidGradleDaemon = !argParser.options.containsKey(FlutterOptions.kAndroidGradleDaemon)
      || boolArg(FlutterOptions.kAndroidGradleDaemon);

1037 1038 1039 1040
    final List<String> androidProjectArgs = argParser.options.containsKey(FlutterOptions.kAndroidProjectArgs)
      ? stringsArg(FlutterOptions.kAndroidProjectArgs)
      : <String>[];

1041 1042 1043 1044 1045 1046
    if (dartObfuscation && (splitDebugInfoPath == null || splitDebugInfoPath.isEmpty)) {
      throwToolExit(
        '"--${FlutterOptions.kDartObfuscationOption}" can only be used in '
        'combination with "--${FlutterOptions.kSplitDebugInfoOption}"',
      );
    }
1047
    final BuildMode buildMode = forcedBuildMode ?? getBuildMode();
1048
    if (buildMode != BuildMode.release && codeSizeDirectory != null) {
1049
      throwToolExit('"--${FlutterOptions.kAnalyzeSize}" can only be used on release builds.');
1050
    }
1051
    if (codeSizeDirectory != null && splitDebugInfoPath != null) {
1052
      throwToolExit('"--${FlutterOptions.kAnalyzeSize}" cannot be combined with "--${FlutterOptions.kSplitDebugInfoOption}".');
1053
    }
1054

1055
    final bool treeShakeIcons = argParser.options.containsKey('tree-shake-icons')
1056
      && buildMode.isPrecompiled == true
1057
      && boolArg('tree-shake-icons');
1058

1059
    final String? bundleSkSLPath = argParser.options.containsKey(FlutterOptions.kBundleSkSLPathOption)
1060 1061 1062
      ? stringArg(FlutterOptions.kBundleSkSLPathOption)
      : null;

1063 1064 1065 1066
    if (bundleSkSLPath != null && !globals.fs.isFileSync(bundleSkSLPath)) {
      throwToolExit('No SkSL shader bundle found at $bundleSkSLPath.');
    }

1067
    final String? performanceMeasurementFile = argParser.options.containsKey(FlutterOptions.kPerformanceMeasurementFile)
1068 1069 1070
      ? stringArg(FlutterOptions.kPerformanceMeasurementFile)
      : null;

1071
    List<String> dartDefines = argParser.options.containsKey(FlutterOptions.kDartDefinesOption)
1072 1073 1074
        ? stringsArg(FlutterOptions.kDartDefinesOption)
        : <String>[];

1075
    if (argParser.options.containsKey('web-renderer')) {
1076
      dartDefines = updateDartDefines(dartDefines, stringArg('web-renderer')!);
1077 1078
    }

1079
    return BuildInfo(buildMode,
1080
      argParser.options.containsKey('flavor')
1081
        ? stringArg('flavor')
1082
        : null,
1083
      trackWidgetCreation: trackWidgetCreation,
1084
      extraFrontEndOptions: extraFrontEndOptions.isNotEmpty
1085 1086
        ? extraFrontEndOptions
        : null,
1087
      extraGenSnapshotOptions: extraGenSnapshotOptions.isNotEmpty
1088
        ? extraGenSnapshotOptions
1089
        : null,
1090 1091
      fileSystemRoots: fileSystemRoots,
      fileSystemScheme: fileSystemScheme,
1092 1093
      buildNumber: buildNumber,
      buildName: argParser.options.containsKey('build-name')
1094
          ? stringArg('build-name')
1095
          : null,
1096
      treeShakeIcons: treeShakeIcons,
1097 1098
      splitDebugInfoPath: splitDebugInfoPath,
      dartObfuscation: dartObfuscation,
1099
      dartDefines: dartDefines,
1100
      bundleSkSLPath: bundleSkSLPath,
1101
      dartExperiments: experiments,
1102
      performanceMeasurementFile: performanceMeasurementFile,
1103
      packagesPath: packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'),
1104
      nullSafetyMode: nullSafetyMode,
1105
      codeSizeDirectory: codeSizeDirectory,
1106
      androidGradleDaemon: androidGradleDaemon,
1107
      packageConfig: packageConfig,
1108
      androidProjectArgs: androidProjectArgs,
1109 1110 1111
      initializeFromDill: argParser.options.containsKey(FlutterOptions.kInitializeFromDill)
          ? stringArg(FlutterOptions.kInitializeFromDill)
          : null,
1112
    );
1113 1114
  }

1115
  void setupApplicationPackages() {
1116
    applicationPackages ??= ApplicationPackageFactory.instance;
1117 1118
  }

1119
  /// The path to send to Google Analytics. Return null here to disable
1120
  /// tracking of the command.
1121
  Future<String?> get usagePath async {
1122
    if (parent is FlutterCommand) {
1123 1124
      final FlutterCommand? commandParent = parent as FlutterCommand?;
      final String? path = await commandParent?.usagePath;
1125 1126 1127 1128 1129 1130
      // Don't report for parents that return null for usagePath.
      return path == null ? null : '$path/$name';
    } else {
      return name;
    }
  }
1131

1132
  /// Additional usage values to be sent with the usage ping.
1133
  Future<CustomDimensions> get usageValues async => const CustomDimensions();
1134

1135 1136 1137 1138 1139 1140
  /// 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.
1141
  @override
1142
  Future<void> run() {
1143
    final DateTime startTime = globals.systemClock.now();
Devon Carew's avatar
Devon Carew committed
1144

1145
    return context.run<void>(
1146 1147 1148
      name: 'command',
      overrides: <Type, Generator>{FlutterCommand: () => this},
      body: () async {
1149 1150 1151
        if (_usesFatalWarnings) {
          globals.logger.fatalWarnings = boolArg(FlutterOptions.kFatalWarnings);
        }
1152
        // Prints the welcome message if needed.
1153
        globals.flutterUsage.printWelcome();
1154
        _printDeprecationWarning();
1155 1156 1157 1158
        final String? commandPath = await usagePath;
        if (commandPath != null) {
          _registerSignalHandlers(commandPath, startTime);
        }
1159
        FlutterCommandResult commandResult = FlutterCommandResult.fail();
1160
        try {
1161
          commandResult = await verifyThenRunCommand(commandPath);
1162
        } finally {
1163
          final DateTime endTime = globals.systemClock.now();
1164
          globals.printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
1165 1166 1167
          if (commandPath != null) {
            _sendPostUsage(commandPath, commandResult, startTime, endTime);
          }
1168 1169 1170
          if (_usesFatalWarnings) {
            globals.logger.checkForFatalLogs();
          }
1171 1172 1173
        }
      },
    );
Devon Carew's avatar
Devon Carew committed
1174 1175
  }

1176 1177
  void _printDeprecationWarning() {
    if (deprecated) {
1178
      globals.printWarning(
1179 1180 1181
        '${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 '
1182
        'for previous releases of Flutter.\n',
1183
      );
1184 1185 1186
    }
  }

1187
  /// Updates dart-defines based on [webRenderer].
1188 1189 1190 1191 1192 1193
  @visibleForTesting
  static List<String> updateDartDefines(List<String> dartDefines, String webRenderer) {
    final Set<String> dartDefinesSet = dartDefines.toSet();
    if (!dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_AUTO_DETECT='))
        && dartDefines.any((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA='))) {
      dartDefinesSet.removeWhere((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA='));
1194
    }
1195 1196 1197 1198
    final Iterable<String>? webRendererDefine = _webRendererDartDefines[webRenderer];
    if (webRendererDefine != null) {
      dartDefinesSet.addAll(webRendererDefine);
    }
1199
    return dartDefinesSet.toList();
1200 1201
  }

1202
  void _registerSignalHandlers(String commandPath, DateTime startTime) {
1203
    void handler(io.ProcessSignal s) {
1204
      globals.cache.releaseLock();
1205 1206 1207 1208
      _sendPostUsage(
        commandPath,
        const FlutterCommandResult(ExitStatus.killed),
        startTime,
1209
        globals.systemClock.now(),
1210
      );
1211
    }
1212 1213
    globals.signals.addHandler(io.ProcessSignal.sigterm, handler);
    globals.signals.addHandler(io.ProcessSignal.sigint, handler);
1214 1215
  }

1216 1217 1218 1219
  /// 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.
1220 1221 1222 1223 1224 1225
  void _sendPostUsage(
    String commandPath,
    FlutterCommandResult commandResult,
    DateTime startTime,
    DateTime endTime,
  ) {
1226 1227 1228
    if (commandPath == null) {
      return;
    }
1229
    assert(commandResult != null);
1230
    // Send command result.
1231
    CommandResultEvent(commandPath, commandResult.toString()).send();
1232 1233

    // Send timing.
1234
    final List<String> labels = <String>[
1235
      if (commandResult.exitStatus != null)
1236
        getEnumName(commandResult.exitStatus),
1237
      if (commandResult.timingLabelParts?.isNotEmpty ?? false)
1238
        ...?commandResult.timingLabelParts,
1239
    ];
1240 1241

    final String label = labels
1242
        .where((String label) => !_isBlank(label))
1243
        .join('-');
1244
    globals.flutterUsage.sendTiming(
1245 1246 1247 1248
      'flutter',
      name,
      // If the command provides its own end time, use it. Otherwise report
      // the duration of the entire execution.
1249
      (commandResult.endTimeOverride ?? endTime).difference(startTime),
1250 1251 1252 1253 1254 1255
      // 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,
    );
  }

1256 1257 1258 1259 1260 1261 1262 1263
  /// 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
1264
  Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async {
1265
    globals.preRunValidator.validate();
1266 1267
    // 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.
1268
    if (shouldUpdateCache) {
1269
      // First always update universal artifacts, as some of these (e.g.
1270
      // ios-deploy on macOS) are required to determine `requiredArtifacts`.
1271 1272
      await globals.cache.updateAll(<DevelopmentArtifact>{DevelopmentArtifact.universal});
      await globals.cache.updateAll(await requiredArtifacts);
1273
    }
1274
    globals.cache.releaseLock();
1275

1276 1277
    await validateCommand();

1278 1279 1280
    final FlutterProject project = FlutterProject.current();
    project.checkForDeprecation(deprecationBehavior: deprecationBehavior);

1281
    if (shouldRunPub) {
1282
      final Environment environment = Environment(
1283
        artifacts: globals.artifacts!,
1284 1285 1286 1287 1288 1289 1290
        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,
1291
        platform: globals.platform,
1292
        projectDir: project.directory,
1293
        generateDartPluginRegistry: true,
1294 1295 1296 1297
      );

      await generateLocalizationsSyntheticPackage(
        environment: environment,
1298
        buildSystem: globals.buildSystem!,
1299 1300
      );

1301 1302 1303
      await pub.get(
        context: PubContext.getVerifyContext(name),
        generateSyntheticPackage: project.manifest.generateSyntheticPackage,
1304
        checkUpToDate: cachePubGet,
1305
      );
1306
      await project.regeneratePlatformSpecificTooling();
1307 1308 1309
      if (reportNullSafety) {
        await _sendNullSafetyAnalyticsEvents(project);
      }
1310
    }
1311

1312
    setupApplicationPackages();
Devon Carew's avatar
Devon Carew committed
1313

1314
    if (commandPath != null) {
1315 1316 1317
      Usage.command(commandPath, parameters: CustomDimensions(
        commandHasTerminal: globals.stdio.hasTerminal,
      ).merge(await usageValues));
1318 1319
    }

1320
    return runCommand();
1321 1322
  }

1323 1324 1325 1326 1327 1328 1329 1330 1331 1332
  Future<void> _sendNullSafetyAnalyticsEvents(FlutterProject project) async {
    final BuildInfo buildInfo = await getBuildInfo();
    NullSafetyAnalysisEvent(
      buildInfo.packageConfig,
      buildInfo.nullSafetyMode,
      project.manifest.appName,
      globals.flutterUsage,
    ).send();
  }

1333 1334
  /// The set of development artifacts required for this command.
  ///
1335 1336 1337
  /// Defaults to an empty set. Including [DevelopmentArtifact.universal] is
  /// not required as it is always updated.
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
1338

1339
  /// Subclasses must implement this to execute the command.
1340
  /// Optionally provide a [FlutterCommandResult] to send more details about the
1341 1342
  /// execution for analytics.
  Future<FlutterCommandResult> runCommand();
1343

1344
  /// Find and return all target [Device]s based upon currently connected
1345
  /// devices and criteria entered by the user on the command line.
1346
  /// If no device can be found that meets specified criteria,
1347
  /// then print an error message and return null.
1348
  Future<List<Device>?> findAllTargetDevices({
1349 1350
    bool includeUnsupportedDevices = false,
  }) async {
1351
    if (!globals.doctor!.canLaunchAnything) {
1352
      globals.printError(userMessages.flutterNoDevelopmentDevice);
1353 1354
      return null;
    }
1355
    final DeviceManager deviceManager = globals.deviceManager!;
1356 1357 1358 1359
    List<Device> devices = await deviceManager.findTargetDevices(
      includeUnsupportedDevices ? null : FlutterProject.current(),
      timeout: deviceDiscoveryTimeout,
    );
1360 1361

    if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
1362
      globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId!));
1363 1364
      return null;
    } else if (devices.isEmpty) {
1365 1366 1367 1368 1369 1370 1371 1372 1373 1374
      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(
1375
          (await Device.descriptions(unsupportedDevices))
1376 1377 1378 1379
              .map((String desc) => desc)
              .toList(),
          '\n',
        );
1380
        result.writeln();
1381 1382 1383 1384 1385
        result.writeln(userMessages.flutterMissPlatformProjects(
          Device.devicesPlatformTypes(unsupportedDevices),
        ));
        globals.printStatus(result.toString());
      }
1386
      return null;
1387
    } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
1388
      if (deviceManager.hasSpecifiedDeviceId) {
1389
       globals.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId!));
1390
      } else {
1391
        globals.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
1392
        devices = await deviceManager.getAllConnectedDevices();
1393
      }
1394
      globals.printStatus('');
1395
      await Device.printDevices(devices, globals.logger);
1396 1397
      return null;
    }
1398 1399 1400 1401 1402 1403
    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,
1404
  /// then print an error message and return null.
1405 1406 1407
  ///
  /// If [includeUnsupportedDevices] is true, the tool does not filter
  /// the list by the current project support list.
1408
  Future<Device?> findTargetDevice({
1409 1410
    bool includeUnsupportedDevices = false,
  }) async {
1411
    List<Device>? deviceList = await findAllTargetDevices(includeUnsupportedDevices: includeUnsupportedDevices);
1412
    if (deviceList == null) {
1413
      return null;
1414
    }
1415
    if (deviceList.length > 1) {
1416
      globals.printStatus(userMessages.flutterSpecifyDevice);
1417
      deviceList = await globals.deviceManager!.getAllConnectedDevices();
1418
      globals.printStatus('');
1419
      await Device.printDevices(deviceList, globals.logger);
1420 1421 1422
      return null;
    }
    return deviceList.single;
1423 1424
  }

1425 1426
  @protected
  @mustCallSuper
1427
  Future<void> validateCommand() async {
1428
    if (_requiresPubspecYaml && globalResults?.wasParsed('packages') != true) {
1429
      // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443

      // 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}');
1444
      }
1445
    }
1446 1447

    if (_usesTargetOption) {
1448
      final String targetPath = targetFile;
1449
      if (!globals.fs.isFileSync(targetPath)) {
1450
        throw ToolExit(userMessages.flutterTargetFileMissing(targetPath));
1451
      }
1452 1453
    }
  }
1454

1455 1456 1457 1458 1459 1460 1461
  @override
  String get usage {
    final String usageWithoutDescription = super.usage.substring(
      // The description plus two newlines.
      description.length + 2,
    );
    final String help = <String>[
1462
      if (deprecated)
1463
        '${globals.logger.terminal.warningMark} Deprecated. This command will be removed in a future version of Flutter.',
1464 1465 1466
      description,
      '',
      'Global options:',
1467
      '${runner?.argParser.usage}',
1468 1469 1470 1471 1472 1473
      '',
      usageWithoutDescription,
    ].join('\n');
    return help;
  }

1474
  ApplicationPackageFactory? applicationPackages;
1475 1476

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

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

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

1486 1487 1488 1489 1490 1491 1492 1493
/// 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.
1494
    final List<Device> devices = await globals.deviceManager!.getDevices();
1495 1496 1497 1498 1499 1500
    if (devices.isEmpty) {
      return super.requiredArtifacts;
    }
    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
      DevelopmentArtifact.universal,
    };
1501
    for (final Device device in devices) {
1502
      final TargetPlatform targetPlatform = await device.targetPlatform;
1503
      final DevelopmentArtifact? developmentArtifact = artifactFromTargetPlatform(targetPlatform);
1504 1505
      if (developmentArtifact != null) {
        artifacts.add(developmentArtifact);
1506 1507 1508 1509 1510 1511
      }
    }
    return artifacts;
  }
}

1512 1513
// Returns the development artifact for the target platform, or null
// if none is supported
1514
@protected
1515
DevelopmentArtifact? artifactFromTargetPlatform(TargetPlatform targetPlatform) {
1516
  switch (targetPlatform) {
1517
    case TargetPlatform.android:
1518 1519 1520 1521
    case TargetPlatform.android_arm:
    case TargetPlatform.android_arm64:
    case TargetPlatform.android_x64:
    case TargetPlatform.android_x86:
1522
      return DevelopmentArtifact.androidGenSnapshot;
1523
    case TargetPlatform.web_javascript:
1524 1525 1526
      return DevelopmentArtifact.web;
    case TargetPlatform.ios:
      return DevelopmentArtifact.iOS;
1527
    case TargetPlatform.darwin:
1528
      if (featureFlags.isMacOSEnabled) {
1529 1530 1531
        return DevelopmentArtifact.macOS;
      }
      return null;
1532
    case TargetPlatform.windows_x64:
1533
      if (featureFlags.isWindowsEnabled) {
1534 1535 1536
        return DevelopmentArtifact.windows;
      }
      return null;
1537
    case TargetPlatform.linux_x64:
1538
    case TargetPlatform.linux_arm64:
1539
      if (featureFlags.isLinuxEnabled) {
1540 1541 1542
        return DevelopmentArtifact.linux;
      }
      return null;
1543 1544
    case TargetPlatform.fuchsia_arm64:
    case TargetPlatform.fuchsia_x64:
1545
    case TargetPlatform.tester:
1546
    case TargetPlatform.windows_uwp_x64:
1547 1548 1549
      if (featureFlags.isWindowsUwpEnabled) {
        return DevelopmentArtifact.windowsUwp;
      }
1550 1551 1552
      return null;
  }
}
1553 1554 1555

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