Reland: Ensure that flutter run/drive/test/update_packages only downloads required artifacts  (#30254)

diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart
index 1a015ae..3d8ca6b 100644
--- a/packages/flutter_tools/lib/src/commands/analyze.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -65,7 +65,7 @@
   String get description => "Analyze the project's Dart code.";
 
   @override
-  Set<DevelopmentArtifact> get requiredArtifacts => const <DevelopmentArtifact>{
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
     DevelopmentArtifact.universal,
   };
 
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index 263f88d..ea32069 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -16,7 +16,7 @@
 import '../runner/flutter_command.dart';
 import 'build.dart';
 
-class BuildAotCommand extends BuildSubCommand {
+class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmentArtifacts {
   BuildAotCommand() {
     usesTargetOption();
     addBuildModeFlags();
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 2660932..8a2ac3c 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -6,7 +6,7 @@
 
 import '../android/apk.dart';
 import '../project.dart';
-import '../runner/flutter_command.dart' show FlutterCommandResult;
+import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
 import 'build.dart';
 
 class BuildApkCommand extends BuildSubCommand {
@@ -35,6 +35,12 @@
   final String name = 'apk';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+    DevelopmentArtifact.android,
+  };
+
+  @override
   final String description = 'Build an Android APK file from your app.\n\n'
     'This command can build debug and release versions of your application. \'debug\' builds support '
     'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are '
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index b90e332..f82dab9 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -10,7 +10,7 @@
 import '../build_info.dart';
 import '../globals.dart';
 import '../ios/mac.dart';
-import '../runner/flutter_command.dart' show FlutterCommandResult;
+import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
 import 'build.dart';
 
 class BuildIOSCommand extends BuildSubCommand {
@@ -49,6 +49,12 @@
   final String description = 'Build an iOS application bundle (Mac OS X host only).';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+    DevelopmentArtifact.iOS,
+  };
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     final bool forSimulator = argResults['simulator'];
     defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
index 0bb0d9b..8a1fd9e 100644
--- a/packages/flutter_tools/lib/src/commands/build_web.dart
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -20,7 +20,7 @@
   }
 
   @override
-  Set<DevelopmentArtifact> get requiredArtifacts => const <DevelopmentArtifact>{
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
     DevelopmentArtifact.universal,
     DevelopmentArtifact.web,
   };
diff --git a/packages/flutter_tools/lib/src/commands/channel.dart b/packages/flutter_tools/lib/src/commands/channel.dart
index 1b0bd0f..d9672f6 100644
--- a/packages/flutter_tools/lib/src/commands/channel.dart
+++ b/packages/flutter_tools/lib/src/commands/channel.dart
@@ -32,6 +32,9 @@
   String get invocation => '${runner.executableName} $name [<channel-name>]';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     switch (argResults.rest.length) {
       case 0:
diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart
index c427e39..65682e8 100644
--- a/packages/flutter_tools/lib/src/commands/clean.dart
+++ b/packages/flutter_tools/lib/src/commands/clean.dart
@@ -23,6 +23,9 @@
   final String description = 'Delete the build/ and .dart_tool/ directories.';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     final FlutterProject flutterProject = await FlutterProject.current();
     final Directory buildDir = fs.directory(getBuildDirectory());
diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart
index dd25ec7..89d0142 100644
--- a/packages/flutter_tools/lib/src/commands/config.dart
+++ b/packages/flutter_tools/lib/src/commands/config.dart
@@ -45,6 +45,9 @@
   bool get shouldUpdateCache => false;
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
+  @override
   String get usageFooter {
     // List all config settings.
     String values = config.keys.map<String>((String key) {
diff --git a/packages/flutter_tools/lib/src/commands/format.dart b/packages/flutter_tools/lib/src/commands/format.dart
index 9d5d56e1..1b0c407 100644
--- a/packages/flutter_tools/lib/src/commands/format.dart
+++ b/packages/flutter_tools/lib/src/commands/format.dart
@@ -40,6 +40,11 @@
   final String description = 'Format one or more dart files.';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
+  @override
   String get invocation => '${runner.executableName} $name <one or more paths>';
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/generate.dart b/packages/flutter_tools/lib/src/commands/generate.dart
index d1026ac..2694e2d 100644
--- a/packages/flutter_tools/lib/src/commands/generate.dart
+++ b/packages/flutter_tools/lib/src/commands/generate.dart
@@ -22,6 +22,11 @@
   bool get hidden => true;
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     Cache.releaseLockEarly();
     final FlutterProject flutterProject = await FlutterProject.current();
diff --git a/packages/flutter_tools/lib/src/commands/ide_config.dart b/packages/flutter_tools/lib/src/commands/ide_config.dart
index 2a64070..ad10340 100644
--- a/packages/flutter_tools/lib/src/commands/ide_config.dart
+++ b/packages/flutter_tools/lib/src/commands/ide_config.dart
@@ -43,6 +43,9 @@
   final String name = 'ide-config';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
+  @override
   final String description = 'Configure the IDE for use in the Flutter tree.\n\n'
       'If run on a Flutter tree that is already configured for the IDE, this '
       'command will add any new configurations, recreate any files that are '
diff --git a/packages/flutter_tools/lib/src/commands/inject_plugins.dart b/packages/flutter_tools/lib/src/commands/inject_plugins.dart
index 4acc452..d13c19b 100644
--- a/packages/flutter_tools/lib/src/commands/inject_plugins.dart
+++ b/packages/flutter_tools/lib/src/commands/inject_plugins.dart
@@ -24,6 +24,9 @@
   final bool hidden;
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     final FlutterProject project = await FlutterProject.current();
     refreshPluginsList(project);
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index ab43906..5e0d43c 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -11,7 +11,7 @@
 import '../globals.dart';
 import '../runner/flutter_command.dart';
 
-class InstallCommand extends FlutterCommand {
+class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
   InstallCommand() {
     requiresPubspecYaml();
   }
diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart
index 3e0ec00..c97c62c 100644
--- a/packages/flutter_tools/lib/src/commands/logs.dart
+++ b/packages/flutter_tools/lib/src/commands/logs.dart
@@ -26,6 +26,9 @@
   @override
   final String description = 'Show log output for running Flutter apps.';
 
+  @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
+
   Device device;
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index befbe87..e31b3ad 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -28,6 +28,11 @@
   final String description = 'Commands for managing Flutter packages.';
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
+  @override
   Future<FlutterCommandResult> runCommand() async => null;
 }
 
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 00c2792..e013079 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -20,7 +20,7 @@
 import '../tracing.dart';
 import 'daemon.dart';
 
-abstract class RunCommandBase extends FlutterCommand {
+abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
   // Used by run and drive commands.
   RunCommandBase({ bool verboseHelp = false }) {
     addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp);
@@ -64,6 +64,7 @@
   }
 
   bool get traceStartup => argResults['trace-startup'];
+
   String get route => argResults['route'];
 }
 
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 61a6510..2dc9d6f 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -87,6 +87,11 @@
   }
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
+  @override
   String get name => 'test';
 
   @override
@@ -94,7 +99,7 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
-    await cache.updateAll(requiredArtifacts);
+    await cache.updateAll(await requiredArtifacts);
     if (!fs.isFileSync('pubspec.yaml')) {
       throwToolExit(
         'Error: No pubspec.yaml file found in the current working directory.\n'
diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart
index c6f8fb3..d18123e 100644
--- a/packages/flutter_tools/lib/src/commands/update_packages.dart
+++ b/packages/flutter_tools/lib/src/commands/update_packages.dart
@@ -84,6 +84,11 @@
   @override
   final bool hidden;
 
+  @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
   Future<void> _downloadCoverageData() async {
     final Status status = logger.startProgress(
       'Downloading lcov data for package:flutter...',
diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index f39e33b..1b7cf7a 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -37,6 +37,11 @@
   bool get shouldUpdateCache => false;
 
   @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+  };
+
+  @override
   Future<FlutterCommandResult> runCommand() async {
     final UpgradeCommandRunner upgradeCommandRunner = UpgradeCommandRunner();
     await upgradeCommandRunner.runCommand(argResults['force'], GitTagVersion.determine(), FlutterVersion.instance);
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index dd84802..19c7711 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -540,7 +540,7 @@
     // 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.
     if (shouldUpdateCache) {
-      await cache.updateAll(requiredArtifacts);
+      await cache.updateAll(await requiredArtifacts);
     }
 
     if (shouldRunPub) {
@@ -563,7 +563,7 @@
   ///
   /// Defaults to [DevelopmentArtifact.universal],
   /// [DevelopmentArtifact.android], and [DevelopmentArtifact.iOS].
-  Set<DevelopmentArtifact> get requiredArtifacts => const <DevelopmentArtifact>{
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
     DevelopmentArtifact.universal,
     DevelopmentArtifact.iOS,
     DevelopmentArtifact.android,
@@ -689,6 +689,90 @@
   ApplicationPackageStore applicationPackages;
 }
 
+/// 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.
+    final List<Device> devices = await deviceManager.getDevices().toList();
+    if (devices.isEmpty) {
+      return super.requiredArtifacts;
+    }
+    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+    };
+    for (Device device in devices) {
+      final TargetPlatform targetPlatform = await device.targetPlatform;
+      switch (targetPlatform) {
+        case TargetPlatform.android_arm:
+        case TargetPlatform.android_arm64:
+        case TargetPlatform.android_x64:
+        case TargetPlatform.android_x86:
+          artifacts.add(DevelopmentArtifact.android);
+          break;
+        case TargetPlatform.web:
+          artifacts.add(DevelopmentArtifact.web);
+          break;
+        case TargetPlatform.ios:
+          artifacts.add(DevelopmentArtifact.iOS);
+          break;
+        case TargetPlatform.darwin_x64:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.tester:
+        case TargetPlatform.windows_x64:
+        case TargetPlatform.linux_x64:
+          // No artifacts currently supported.
+          break;
+      }
+    }
+    return artifacts;
+  }
+}
+
+/// A mixin which applies an implementation of [requiredArtifacts] that only
+/// downloads artifacts corresponding to a target device.
+mixin TargetPlatformBasedDevelopmentArtifacts on FlutterCommand {
+  @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
+    // If there is no specified target device, fallback to the default
+    // confiugration.
+    final String rawTargetPlatform = argResults['target-platform'];
+    final TargetPlatform targetPlatform = getTargetPlatformForName(rawTargetPlatform);
+    if (targetPlatform == null) {
+      return super.requiredArtifacts;
+    }
+
+    final Set<DevelopmentArtifact> artifacts = <DevelopmentArtifact>{
+      DevelopmentArtifact.universal,
+    };
+    switch (targetPlatform) {
+      case TargetPlatform.android_arm:
+      case TargetPlatform.android_arm64:
+      case TargetPlatform.android_x64:
+      case TargetPlatform.android_x86:
+        artifacts.add(DevelopmentArtifact.android);
+        break;
+      case TargetPlatform.web:
+        artifacts.add(DevelopmentArtifact.web);
+        break;
+      case TargetPlatform.ios:
+        artifacts.add(DevelopmentArtifact.iOS);
+        break;
+      case TargetPlatform.darwin_x64:
+      case TargetPlatform.fuchsia:
+      case TargetPlatform.tester:
+      case TargetPlatform.windows_x64:
+      case TargetPlatform.linux_x64:
+        // No artifacts currently supported.
+        break;
+    }
+    return artifacts;
+  }
+}
+
 /// A command which runs less analytics and checks to speed up startup time.
 abstract class FastFlutterCommand extends FlutterCommand {
   @override
diff --git a/packages/flutter_tools/test/commands/run_test.dart b/packages/flutter_tools/test/commands/run_test.dart
index 16c4efe..497b572 100644
--- a/packages/flutter_tools/test/commands/run_test.dart
+++ b/packages/flutter_tools/test/commands/run_test.dart
@@ -3,13 +3,19 @@
 // found in the LICENSE file.
 
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/commands/run.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
 import '../src/context.dart';
 import '../src/mocks.dart';
 
 void main() {
+  final MockDeviceManager mockDeviceManager = MockDeviceManager();
+
   group('run', () {
     testUsingContext('fails when target not found', () async {
       final RunCommand command = RunCommand();
@@ -21,5 +27,65 @@
         expect(e.exitCode ?? 1, 1);
       }
     });
+
+    testUsingContext('should only request artifacts corresponding to connected devices', () async {
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.android_arm),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.android,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.ios),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.iOS,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.ios),
+          MockDevice(TargetPlatform.android_arm),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.iOS,
+        DevelopmentArtifact.android,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.web),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.web,
+      }));
+    }, overrides: <Type, Generator>{
+      DeviceManager: () => mockDeviceManager,
+    });
   });
 }
+
+class MockDeviceManager extends Mock implements DeviceManager {}
+class MockDevice extends Mock implements Device {
+  MockDevice(this._targetPlatform);
+
+  final TargetPlatform _targetPlatform;
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => _targetPlatform;
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/test/runner/utils.dart b/packages/flutter_tools/test/runner/utils.dart
index b7b1ec1..e69e50d 100644
--- a/packages/flutter_tools/test/runner/utils.dart
+++ b/packages/flutter_tools/test/runner/utils.dart
@@ -14,7 +14,7 @@
 class DummyFlutterCommand extends FlutterCommand {
 
   DummyFlutterCommand({
-    this.shouldUpdateCache  = false,
+    this.shouldUpdateCache = false,
     this.noUsagePath  = false,
     this.commandFunction,
   });