• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:args/args.dart';
8import 'package:args/command_runner.dart';
9import 'package:meta/meta.dart';
10import 'package:quiver/strings.dart';
11
12import '../application_package.dart';
13import '../base/common.dart';
14import '../base/context.dart';
15import '../base/file_system.dart';
16import '../base/io.dart' as io;
17import '../base/terminal.dart';
18import '../base/time.dart';
19import '../base/user_messages.dart';
20import '../base/utils.dart';
21import '../build_info.dart';
22import '../bundle.dart' as bundle;
23import '../cache.dart';
24import '../dart/package_map.dart';
25import '../dart/pub.dart';
26import '../device.dart';
27import '../doctor.dart';
28import '../features.dart';
29import '../globals.dart';
30import '../project.dart';
31import '../reporting/reporting.dart';
32import 'flutter_command_runner.dart';
33
34export '../cache.dart' show DevelopmentArtifact;
35
36enum ExitStatus {
37  success,
38  warning,
39  fail,
40}
41
42/// [FlutterCommand]s' subclasses' [FlutterCommand.runCommand] can optionally
43/// provide a [FlutterCommandResult] to furnish additional information for
44/// analytics.
45class FlutterCommandResult {
46  const FlutterCommandResult(
47    this.exitStatus, {
48    this.timingLabelParts,
49    this.endTimeOverride,
50  });
51
52  final ExitStatus exitStatus;
53
54  /// Optional data that can be appended to the timing event.
55  /// https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#timingLabel
56  /// Do not add PII.
57  final List<String> timingLabelParts;
58
59  /// Optional epoch time when the command's non-interactive wait time is
60  /// complete during the command's execution. Use to measure user perceivable
61  /// latency without measuring user interaction time.
62  ///
63  /// [FlutterCommand] will automatically measure and report the command's
64  /// complete time if not overridden.
65  final DateTime endTimeOverride;
66
67  @override
68  String toString() {
69    switch (exitStatus) {
70      case ExitStatus.success:
71        return 'success';
72      case ExitStatus.warning:
73        return 'warning';
74      case ExitStatus.fail:
75        return 'fail';
76      default:
77        assert(false);
78        return null;
79    }
80  }
81}
82
83/// Common flutter command line options.
84class FlutterOptions {
85  static const String kExtraFrontEndOptions = 'extra-front-end-options';
86  static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options';
87  static const String kEnableExperiment = 'enable-experiment';
88  static const String kFileSystemRoot = 'filesystem-root';
89  static const String kFileSystemScheme = 'filesystem-scheme';
90}
91
92abstract class FlutterCommand extends Command<void> {
93  /// The currently executing command (or sub-command).
94  ///
95  /// Will be `null` until the top-most command has begun execution.
96  static FlutterCommand get current => context.get<FlutterCommand>();
97
98  /// The option name for a custom observatory port.
99  static const String observatoryPortOption = 'observatory-port';
100
101  /// The flag name for whether or not to use ipv6.
102  static const String ipv6Flag = 'ipv6';
103
104  @override
105  ArgParser get argParser => _argParser;
106  final ArgParser _argParser = ArgParser(
107    allowTrailingOptions: false,
108    usageLineLength: outputPreferences.wrapText ? outputPreferences.wrapColumn : null,
109  );
110
111  @override
112  FlutterCommandRunner get runner => super.runner;
113
114  bool _requiresPubspecYaml = false;
115
116  /// Whether this command uses the 'target' option.
117  bool _usesTargetOption = false;
118
119  bool _usesPubOption = false;
120
121  bool _usesPortOption = false;
122
123  bool _usesIpv6Flag = false;
124
125  bool get shouldRunPub => _usesPubOption && argResults['pub'];
126
127  bool get shouldUpdateCache => true;
128
129  BuildMode _defaultBuildMode;
130
131  void requiresPubspecYaml() {
132    _requiresPubspecYaml = true;
133  }
134
135  void usesTargetOption() {
136    argParser.addOption('target',
137      abbr: 't',
138      defaultsTo: bundle.defaultMainPath,
139      help: 'The main entry-point file of the application, as run on the device.\n'
140            'If the --target option is omitted, but a file name is provided on '
141            'the command line, then that is used instead.',
142      valueHelp: 'path');
143    _usesTargetOption = true;
144  }
145
146  String get targetFile {
147    if (argResults.wasParsed('target'))
148      return argResults['target'];
149    else if (argResults.rest.isNotEmpty)
150      return argResults.rest.first;
151    else
152      return bundle.defaultMainPath;
153  }
154
155  void usesPubOption() {
156    argParser.addFlag('pub',
157      defaultsTo: true,
158      help: 'Whether to run "flutter pub get" before executing this command.');
159    _usesPubOption = true;
160  }
161
162  /// Adds flags for using a specific filesystem root and scheme.
163  ///
164  /// [hide] indicates whether or not to hide these options when the user asks
165  /// for help.
166  void usesFilesystemOptions({ @required bool hide }) {
167    argParser
168      ..addOption('output-dill',
169        hide: hide,
170        help: 'Specify the path to frontend server output kernel file.',
171      )
172      ..addMultiOption(FlutterOptions.kFileSystemRoot,
173        hide: hide,
174        help: 'Specify the path, that is used as root in a virtual file system\n'
175            'for compilation. Input file name should be specified as Uri in\n'
176            'filesystem-scheme scheme. Use only in Dart 2 mode.\n'
177            'Requires --output-dill option to be explicitly specified.\n',
178      )
179      ..addOption(FlutterOptions.kFileSystemScheme,
180        defaultsTo: 'org-dartlang-root',
181        hide: hide,
182        help: 'Specify the scheme that is used for virtual file system used in\n'
183            'compilation. See more details on filesystem-root option.\n',
184      );
185  }
186
187  /// Adds options for connecting to the Dart VM observatory port.
188  void usesPortOptions() {
189    argParser.addOption(observatoryPortOption,
190        help: 'Listen to the given port for an observatory debugger connection.\n'
191              'Specifying port 0 (the default) will find a random free port.',
192    );
193    _usesPortOption = true;
194  }
195
196  /// Gets the observatory port provided to in the 'observatory-port' option.
197  ///
198  /// If no port is set, returns null.
199  int get observatoryPort {
200    if (!_usesPortOption || argResults['observatory-port'] == null) {
201      return null;
202    }
203    try {
204      return int.parse(argResults['observatory-port']);
205    } catch (error) {
206      throwToolExit('Invalid port for `--observatory-port`: $error');
207    }
208    return null;
209  }
210
211  void usesIpv6Flag() {
212    argParser.addFlag(ipv6Flag,
213      hide: true,
214      negatable: false,
215      help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool '
216            'forwards the host port to a device port. Not used when the '
217            '--debug-port flag is not set.',
218    );
219    _usesIpv6Flag = true;
220  }
221
222  bool get ipv6 => _usesIpv6Flag ? argResults['ipv6'] : null;
223
224  void usesBuildNumberOption() {
225    argParser.addOption('build-number',
226        help: 'An identifier used as an internal version number.\n'
227              'Each build must have a unique identifier to differentiate it from previous builds.\n'
228              'It is used to determine whether one build is more recent than another, with higher numbers indicating more recent build.\n'
229              'On Android it is used as \'versionCode\'.\n'
230              'On Xcode builds it is used as \'CFBundleVersion\'',
231    );
232  }
233
234  void usesBuildNameOption() {
235    argParser.addOption('build-name',
236        help: 'A "x.y.z" string used as the version number shown to users.\n'
237              'For each new version of your app, you will provide a version number to differentiate it from previous versions.\n'
238              'On Android it is used as \'versionName\'.\n'
239              'On Xcode builds it is used as \'CFBundleShortVersionString\'',
240        valueHelp: 'x.y.z');
241  }
242
243  void usesIsolateFilterOption({ @required bool hide }) {
244    argParser.addOption('isolate-filter',
245      defaultsTo: null,
246      hide: hide,
247      help: 'Restricts commands to a subset of the available isolates (running instances of Flutter).\n'
248            'Normally there\'s only one, but when adding Flutter to a pre-existing app it\'s possible to create multiple.');
249  }
250
251  void addBuildModeFlags({ bool defaultToRelease = true, bool verboseHelp = false }) {
252    defaultBuildMode = defaultToRelease ? BuildMode.release : BuildMode.debug;
253
254    argParser.addFlag('debug',
255      negatable: false,
256      help: 'Build a debug version of your app${defaultToRelease ? '' : ' (default mode)'}.');
257    argParser.addFlag('profile',
258      negatable: false,
259      help: 'Build a version of your app specialized for performance profiling.');
260    argParser.addFlag('release',
261      negatable: false,
262      help: 'Build a release version of your app${defaultToRelease ? ' (default mode)' : ''}.');
263  }
264
265  void usesFuchsiaOptions({ bool hide = false }) {
266    argParser.addOption(
267      'target-model',
268      help: 'Target model that determines what core libraries are available',
269      defaultsTo: 'flutter',
270      hide: hide,
271      allowed: const <String>['flutter', 'flutter_runner'],
272    );
273    argParser.addOption(
274      'module',
275      abbr: 'm',
276      hide: hide,
277      help: 'The name of the module (required if attaching to a fuchsia device)',
278      valueHelp: 'module-name',
279    );
280  }
281
282  set defaultBuildMode(BuildMode value) {
283    _defaultBuildMode = value;
284  }
285
286  BuildMode getBuildMode() {
287    final List<bool> modeFlags = <bool>[argResults['debug'], argResults['profile'], argResults['release']];
288    if (modeFlags.where((bool flag) => flag).length > 1)
289      throw UsageException('Only one of --debug, --profile, or --release can be specified.', null);
290    if (argResults['debug']) {
291      return BuildMode.debug;
292    }
293    if (argResults['profile']) {
294      return BuildMode.profile;
295    }
296    if (argResults['release']) {
297      return BuildMode.release;
298    }
299    return _defaultBuildMode;
300  }
301
302  void usesFlavorOption() {
303    argParser.addOption(
304      'flavor',
305      help: 'Build a custom app flavor as defined by platform-specific build setup.\n'
306            'Supports the use of product flavors in Android Gradle scripts, and '
307            'the use of custom Xcode schemes.',
308    );
309  }
310
311  void usesTrackWidgetCreation({ bool hasEffect = true, @required bool verboseHelp }) {
312    argParser.addFlag(
313      'track-widget-creation',
314      hide: !hasEffect && !verboseHelp,
315      defaultsTo: false, // this will soon be changed to true
316      help: 'Track widget creation locations. This enables features such as the widget inspector. '
317            'This parameter is only functional in debug mode (i.e. when compiling JIT, not AOT).',
318    );
319  }
320
321  BuildInfo getBuildInfo() {
322    final bool trackWidgetCreation = argParser.options.containsKey('track-widget-creation')
323        ? argResults['track-widget-creation']
324        : false;
325
326    final String buildNumber = argParser.options.containsKey('build-number') && argResults['build-number'] != null
327        ? argResults['build-number']
328        : null;
329
330    String extraFrontEndOptions =
331        argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
332            ? argResults[FlutterOptions.kExtraFrontEndOptions]
333            : null;
334    if (argParser.options.containsKey(FlutterOptions.kEnableExperiment) &&
335        argResults[FlutterOptions.kEnableExperiment] != null) {
336      for (String expFlag in argResults[FlutterOptions.kEnableExperiment]) {
337        final String flag = '--enable-experiment=' + expFlag;
338        if (extraFrontEndOptions != null) {
339          extraFrontEndOptions += ',' + flag;
340        } else {
341          extraFrontEndOptions = flag;
342        }
343      }
344    }
345
346    return BuildInfo(getBuildMode(),
347      argParser.options.containsKey('flavor')
348        ? argResults['flavor']
349        : null,
350      trackWidgetCreation: trackWidgetCreation,
351      extraFrontEndOptions: extraFrontEndOptions,
352      extraGenSnapshotOptions: argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
353          ? argResults[FlutterOptions.kExtraGenSnapshotOptions]
354          : null,
355      fileSystemRoots: argParser.options.containsKey(FlutterOptions.kFileSystemRoot)
356          ? argResults[FlutterOptions.kFileSystemRoot] : null,
357      fileSystemScheme: argParser.options.containsKey(FlutterOptions.kFileSystemScheme)
358          ? argResults[FlutterOptions.kFileSystemScheme] : null,
359      buildNumber: buildNumber,
360      buildName: argParser.options.containsKey('build-name')
361          ? argResults['build-name']
362          : null,
363    );
364  }
365
366  void setupApplicationPackages() {
367    applicationPackages ??= ApplicationPackageStore();
368  }
369
370  /// The path to send to Google Analytics. Return null here to disable
371  /// tracking of the command.
372  Future<String> get usagePath async {
373    if (parent is FlutterCommand) {
374      final FlutterCommand commandParent = parent;
375      final String path = await commandParent.usagePath;
376      // Don't report for parents that return null for usagePath.
377      return path == null ? null : '$path/$name';
378    } else {
379      return name;
380    }
381  }
382
383  /// Additional usage values to be sent with the usage ping.
384  Future<Map<CustomDimensions, String>> get usageValues async =>
385      const <CustomDimensions, String>{};
386
387  /// Runs this command.
388  ///
389  /// Rather than overriding this method, subclasses should override
390  /// [verifyThenRunCommand] to perform any verification
391  /// and [runCommand] to execute the command
392  /// so that this method can record and report the overall time to analytics.
393  @override
394  Future<void> run() {
395    final DateTime startTime = systemClock.now();
396
397    return context.run<void>(
398      name: 'command',
399      overrides: <Type, Generator>{FlutterCommand: () => this},
400      body: () async {
401        if (flutterUsage.isFirstRun) {
402          flutterUsage.printWelcome();
403        }
404        final String commandPath = await usagePath;
405        FlutterCommandResult commandResult;
406        try {
407          commandResult = await verifyThenRunCommand(commandPath);
408        } on ToolExit {
409          commandResult = const FlutterCommandResult(ExitStatus.fail);
410          rethrow;
411        } finally {
412          final DateTime endTime = systemClock.now();
413          printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
414          _sendPostUsage(commandPath, commandResult, startTime, endTime);
415        }
416      },
417    );
418  }
419
420  /// Logs data about this command.
421  ///
422  /// For example, the command path (e.g. `build/apk`) and the result,
423  /// as well as the time spent running it.
424  void _sendPostUsage(String commandPath, FlutterCommandResult commandResult,
425                      DateTime startTime, DateTime endTime) {
426    if (commandPath == null) {
427      return;
428    }
429
430    // Send command result.
431    CommandResultEvent(commandPath, commandResult).send();
432
433    // Send timing.
434    final List<String> labels = <String>[
435      if (commandResult?.exitStatus != null)
436        getEnumName(commandResult.exitStatus),
437      if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
438        ...commandResult.timingLabelParts,
439    ];
440
441    final String label = labels
442        .where((String label) => !isBlank(label))
443        .join('-');
444    flutterUsage.sendTiming(
445      'flutter',
446      name,
447      // If the command provides its own end time, use it. Otherwise report
448      // the duration of the entire execution.
449      (commandResult?.endTimeOverride ?? endTime).difference(startTime),
450      // Report in the form of `success-[parameter1-parameter2]`, all of which
451      // can be null if the command doesn't provide a FlutterCommandResult.
452      label: label == '' ? null : label,
453    );
454  }
455
456  /// Perform validation then call [runCommand] to execute the command.
457  /// Return a [Future] that completes with an exit code
458  /// indicating whether execution was successful.
459  ///
460  /// Subclasses should override this method to perform verification
461  /// then call this method to execute the command
462  /// rather than calling [runCommand] directly.
463  @mustCallSuper
464  Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
465    await validateCommand();
466
467    // Populate the cache. We call this before pub get below so that the sky_engine
468    // package is available in the flutter cache for pub to find.
469    if (shouldUpdateCache) {
470      await cache.updateAll(await requiredArtifacts);
471    }
472
473    if (shouldRunPub) {
474      await pubGet(context: PubContext.getVerifyContext(name));
475      final FlutterProject project = FlutterProject.current();
476      await project.ensureReadyForPlatformSpecificTooling(checkProjects: true);
477    }
478
479    setupApplicationPackages();
480
481    if (commandPath != null) {
482      final Map<CustomDimensions, String> additionalUsageValues =
483        <CustomDimensions, String>{
484          ...?await usageValues,
485          CustomDimensions.commandHasTerminal: io.stdout.hasTerminal ? 'true' : 'false',
486        };
487      Usage.command(commandPath, parameters: additionalUsageValues);
488    }
489
490    return await runCommand();
491  }
492
493  /// The set of development artifacts required for this command.
494  ///
495  /// Defaults to [DevelopmentArtifact.universal].
496  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
497    DevelopmentArtifact.universal,
498  };
499
500  /// Subclasses must implement this to execute the command.
501  /// Optionally provide a [FlutterCommandResult] to send more details about the
502  /// execution for analytics.
503  Future<FlutterCommandResult> runCommand();
504
505  /// Find and return all target [Device]s based upon currently connected
506  /// devices and criteria entered by the user on the command line.
507  /// If no device can be found that meets specified criteria,
508  /// then print an error message and return null.
509  Future<List<Device>> findAllTargetDevices() async {
510    if (!doctor.canLaunchAnything) {
511      printError(userMessages.flutterNoDevelopmentDevice);
512      return null;
513    }
514
515    List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current());
516
517    if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
518      printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId));
519      return null;
520    } else if (devices.isEmpty && deviceManager.hasSpecifiedAllDevices) {
521      printStatus(userMessages.flutterNoDevicesFound);
522      return null;
523    } else if (devices.isEmpty) {
524      printStatus(userMessages.flutterNoSupportedDevices);
525      return null;
526    } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
527      if (deviceManager.hasSpecifiedDeviceId) {
528        printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId));
529      } else {
530        printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
531        devices = await deviceManager.getAllConnectedDevices().toList();
532      }
533      printStatus('');
534      await Device.printDevices(devices);
535      return null;
536    }
537    return devices;
538  }
539
540  /// Find and return the target [Device] based upon currently connected
541  /// devices and criteria entered by the user on the command line.
542  /// If a device cannot be found that meets specified criteria,
543  /// then print an error message and return null.
544  Future<Device> findTargetDevice() async {
545    List<Device> deviceList = await findAllTargetDevices();
546    if (deviceList == null)
547      return null;
548    if (deviceList.length > 1) {
549      printStatus(userMessages.flutterSpecifyDevice);
550      deviceList = await deviceManager.getAllConnectedDevices().toList();
551      printStatus('');
552      await Device.printDevices(deviceList);
553      return null;
554    }
555    return deviceList.single;
556  }
557
558  void printNoConnectedDevices() {
559    printStatus(userMessages.flutterNoConnectedDevices);
560  }
561
562  @protected
563  @mustCallSuper
564  Future<void> validateCommand() async {
565    if (_requiresPubspecYaml && !PackageMap.isUsingCustomPackagesPath) {
566      // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
567      if (!fs.isFileSync('pubspec.yaml')) {
568        throw ToolExit(userMessages.flutterNoPubspec);
569      }
570
571      if (fs.isFileSync('flutter.yaml')) {
572        throw ToolExit(userMessages.flutterMergeYamlFiles);
573      }
574
575      // Validate the current package map only if we will not be running "pub get" later.
576      if (parent?.name != 'pub' && !(_usesPubOption && argResults['pub'])) {
577        final String error = PackageMap(PackageMap.globalPackagesPath).checkValid();
578        if (error != null)
579          throw ToolExit(error);
580      }
581    }
582
583    if (_usesTargetOption) {
584      final String targetPath = targetFile;
585      if (!fs.isFileSync(targetPath))
586        throw ToolExit(userMessages.flutterTargetFileMissing(targetPath));
587    }
588  }
589
590  ApplicationPackageStore applicationPackages;
591}
592
593/// A mixin which applies an implementation of [requiredArtifacts] that only
594/// downloads artifacts corresponding to an attached device.
595mixin DeviceBasedDevelopmentArtifacts on FlutterCommand {
596  @override
597  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
598    // If there are no attached devices, use the default configuration.
599    // Otherwise, only add development artifacts which correspond to a
600    // connected device.
601    final List<Device> devices = await deviceManager.getDevices().toList();
602    if (devices.isEmpty) {
603      return super.requiredArtifacts;
604    }
605    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
606      DevelopmentArtifact.universal,
607    };
608    for (Device device in devices) {
609      final TargetPlatform targetPlatform = await device.targetPlatform;
610      final DevelopmentArtifact developmentArtifact = _artifactFromTargetPlatform(targetPlatform);
611      if (developmentArtifact != null) {
612        artifacts.add(developmentArtifact);
613      }
614    }
615    return artifacts;
616  }
617}
618
619/// A mixin which applies an implementation of [requiredArtifacts] that only
620/// downloads artifacts corresponding to a target device.
621mixin TargetPlatformBasedDevelopmentArtifacts on FlutterCommand {
622  @override
623  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
624    // If there is no specified target device, fallback to the default
625    // confiugration.
626    final String rawTargetPlatform = argResults['target-platform'];
627    final TargetPlatform targetPlatform = getTargetPlatformForName(rawTargetPlatform);
628    if (targetPlatform == null) {
629      return super.requiredArtifacts;
630    }
631
632    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
633      DevelopmentArtifact.universal,
634    };
635    final DevelopmentArtifact developmentArtifact = _artifactFromTargetPlatform(targetPlatform);
636    if (developmentArtifact != null) {
637      artifacts.add(developmentArtifact);
638    }
639    return artifacts;
640  }
641}
642
643// Returns the development artifact for the target platform, or null
644// if none is supported
645DevelopmentArtifact _artifactFromTargetPlatform(TargetPlatform targetPlatform) {
646  switch (targetPlatform) {
647    case TargetPlatform.android_arm:
648    case TargetPlatform.android_arm64:
649    case TargetPlatform.android_x64:
650    case TargetPlatform.android_x86:
651      return DevelopmentArtifact.android;
652    case TargetPlatform.web_javascript:
653      return DevelopmentArtifact.web;
654    case TargetPlatform.ios:
655      return DevelopmentArtifact.iOS;
656    case TargetPlatform.darwin_x64:
657      if (featureFlags.isMacOSEnabled) {
658        return DevelopmentArtifact.macOS;
659      }
660      return null;
661    case TargetPlatform.windows_x64:
662      if (featureFlags.isWindowsEnabled) {
663        return DevelopmentArtifact.windows;
664      }
665      return null;
666    case TargetPlatform.linux_x64:
667      if (featureFlags.isLinuxEnabled) {
668        return DevelopmentArtifact.linux;
669      }
670      return null;
671    case TargetPlatform.fuchsia:
672    case TargetPlatform.tester:
673      // No artifacts currently supported.
674      return null;
675  }
676  return null;
677}
678
679/// A command which runs less analytics and checks to speed up startup time.
680abstract class FastFlutterCommand extends FlutterCommand {
681  @override
682  Future<void> run() {
683    return context.run<void>(
684      name: 'command',
685      overrides: <Type, Generator>{FlutterCommand: () => this},
686      body: runCommand,
687    );
688  }
689}
690