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