Lazily download artifacts (III) (#27903)

diff --git a/packages/flutter_tools/bin/fuchsia_attach.dart b/packages/flutter_tools/bin/fuchsia_attach.dart
index eb91c2d..30840c4 100644
--- a/packages/flutter_tools/bin/fuchsia_attach.dart
+++ b/packages/flutter_tools/bin/fuchsia_attach.dart
@@ -134,4 +134,7 @@
     Cache.flutterRoot = '$originalWorkingDirectory/third_party/dart-pkg/git/flutter';
     return super.runCommand();
   }
+
+  @override
+  Future<void> updateCache() async {}
 }
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index 1bf2310..89810c9 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -84,7 +84,6 @@
 
   local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}"
 
-  AssertExists "${framework_path}"
   AssertExists "${project_path}"
 
   local derived_dir="${SOURCE_ROOT}/Flutter"
@@ -123,6 +122,12 @@
     flutter_podspec="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.podspec"
   fi
 
+  # If the framework path does not exist, ensure that it is downloaded.
+  if [[ ! -e "$1" ]]; then
+    FLUTTER_ALREADY_LOCKED = "true"
+    RunCommand "${FLUTTER_ROOT}/bin/flutter" precache --suppress-analytics
+  fi
+
   if [[ -e "${project_path}/.ios" ]]; then
     RunCommand rm -rf -- "${derived_dir}/engine"
     mkdir "${derived_dir}/engine"
diff --git a/packages/flutter_tools/lib/src/base/net.dart b/packages/flutter_tools/lib/src/base/net.dart
index 742bd8b..788a0d4 100644
--- a/packages/flutter_tools/lib/src/base/net.dart
+++ b/packages/flutter_tools/lib/src/base/net.dart
@@ -37,7 +37,8 @@
   printTrace('Downloading: $url');
   HttpClient httpClient;
   if (context[HttpClientFactory] != null) {
-    httpClient = (context[HttpClientFactory] as HttpClientFactory)(); // ignore: avoid_as
+    final HttpClientFactory httpClientFactory = context[HttpClientFactory];
+    httpClient = httpClientFactory();
   } else {
     httpClient = HttpClient();
   }
@@ -64,9 +65,9 @@
   // If we're making a HEAD request, we're only checking to see if the URL is
   // valid.
   if (onlyHeaders) {
-    return (response.statusCode == 200) ? <int>[] : null;
+    return (response.statusCode == HttpStatus.ok) ? <int>[] : null;
   }
-  if (response.statusCode != 200) {
+  if (response.statusCode != HttpStatus.ok) {
     if (response.statusCode > 0 && response.statusCode < 500) {
       throwToolExit(
         'Download failed.\n'
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index 7f3aa7a..772a006 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -13,6 +13,7 @@
 import 'base/net.dart';
 import 'base/os.dart';
 import 'base/platform.dart';
+import 'build_info.dart';
 import 'globals.dart';
 
 /// A wrapper around the `bin/cache/` directory.
@@ -187,7 +188,24 @@
     return isOlderThanReference(entity: entity, referenceFile: flutterToolsStamp);
   }
 
-  bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
+  UpdateResult isUpToDate({
+    List<BuildMode> buildModes = const <BuildMode>[],
+    List<TargetPlatform> targetPlatforms = const <TargetPlatform>[],
+    bool skipUnknown = true,
+  }) {
+    bool isUpToDate = true;
+    bool clobber = false;
+    for (CachedArtifact artifact in _artifacts) {
+      final UpdateResult result =  artifact.isUpToDate(
+        buildModes: buildModes,
+        targetPlatforms: targetPlatforms,
+        skipUnknown: skipUnknown,
+      );
+      isUpToDate &= result.isUpToDate;
+      clobber |= result.clobber;
+    }
+    return UpdateResult(isUpToDate: isUpToDate, clobber: clobber);
+  }
 
   Future<String> getThirdPartyFile(String urlStr, String serviceName) async {
     final Uri url = Uri.parse(urlStr);
@@ -210,13 +228,40 @@
     return cachedFile.path;
   }
 
-  Future<void> updateAll() async {
-    if (!_lockEnabled)
+  Future<void> updateAll({
+    List<BuildMode> buildModes = const <BuildMode>[],
+    List<TargetPlatform> targetPlatforms = const <TargetPlatform>[],
+    bool skipUnknown = true,
+    bool clobber = false,
+  }) async {
+    if (!_lockEnabled) {
       return;
+    }
     try {
       for (CachedArtifact artifact in _artifacts) {
-        if (!artifact.isUpToDate())
-          await artifact.update();
+        bool localClobber = clobber;
+        if (localClobber) {
+          await artifact.update(
+            buildModes: buildModes,
+            targetPlatforms: targetPlatforms,
+            skipUnknown: skipUnknown,
+            clobber: localClobber,
+          );
+        }
+        final UpdateResult result = artifact.isUpToDate(
+          buildModes: buildModes,
+          targetPlatforms: targetPlatforms,
+          skipUnknown: skipUnknown,
+        );
+        localClobber |= result.clobber;
+        if (localClobber || !result.isUpToDate) {
+          await artifact.update(
+            buildModes: buildModes,
+            targetPlatforms: targetPlatforms,
+            skipUnknown: skipUnknown,
+            clobber: localClobber,
+          );
+        }
       }
     } on SocketException catch (e) {
       if (_hostsBlockedInChina.contains(e.address?.host)) {
@@ -232,6 +277,15 @@
   }
 }
 
+class UpdateResult {
+  const UpdateResult({this.isUpToDate, this.clobber = false});
+
+  /// Whether the artifact exists and is the correct version.
+  final bool isUpToDate;
+  /// Whether the artifact needs to be redownloaded.
+  final bool clobber;
+}
+
 /// An artifact managed by the cache.
 abstract class CachedArtifact {
   CachedArtifact(this.name, this.cache);
@@ -248,19 +302,39 @@
   /// starting from scratch.
   final List<File> _downloadedFiles = <File>[];
 
-  bool isUpToDate() {
-    if (!location.existsSync())
-      return false;
-    if (version != cache.getStampFor(name))
-      return false;
-    return isUpToDateInner();
+  @mustCallSuper
+  UpdateResult isUpToDate({
+    List<BuildMode> buildModes = const <BuildMode>[],
+    List<TargetPlatform> targetPlatforms = const <TargetPlatform>[],
+    bool skipUnknown = true,
+  }) {
+    if (!location.existsSync()) {
+      return const UpdateResult(isUpToDate: false, clobber: false);
+    }
+    if (version != cache.getStampFor(name)) {
+      return const UpdateResult(isUpToDate: false, clobber: true);
+    }
+    return const UpdateResult(isUpToDate: true, clobber: false);
   }
 
-  Future<void> update() async {
-    if (location.existsSync())
+  Future<void> update({
+    List<BuildMode> buildModes,
+    List<TargetPlatform> targetPlatforms,
+    bool skipUnknown = true,
+    bool clobber = false,
+  }) async {
+    if (location.existsSync() && clobber) {
       location.deleteSync(recursive: true);
-    location.createSync(recursive: true);
-    await updateInner();
+    }
+    if (!location.existsSync()) {
+      location.createSync(recursive: true);
+    }
+    await updateInner(
+      buildModes: buildModes,
+      targetPlatforms: targetPlatforms,
+      skipUnknown: skipUnknown,
+      clobber: clobber,
+    );
     cache.setStampFor(name, version);
     _removeDownloadedFiles();
   }
@@ -268,6 +342,9 @@
   /// Clear any zip/gzip files downloaded.
   void _removeDownloadedFiles() {
     for (File f in _downloadedFiles) {
+      if (!f.existsSync()) {
+        continue;
+      }
       f.deleteSync();
       for (Directory d = f.parent; d.absolute.path != cache.getDownloadDir().absolute.path; d = d.parent) {
         if (d.listSync().isEmpty) {
@@ -279,11 +356,13 @@
     }
   }
 
-  /// Hook method for extra checks for being up-to-date.
-  bool isUpToDateInner() => true;
-
   /// Template method to perform artifact update.
-  Future<void> updateInner();
+  Future<void> updateInner({
+    @required List<BuildMode> buildModes,
+    @required List<TargetPlatform> targetPlatforms,
+    @required bool skipUnknown,
+    @required bool clobber,
+  });
 
   String get _storageBaseUrl {
     final String overrideUrl = platform.environment['FLUTTER_STORAGE_BASE_URL'];
@@ -351,9 +430,16 @@
   MaterialFonts(Cache cache) : super('material_fonts', cache);
 
   @override
-  Future<void> updateInner() {
+  Future<void> updateInner({
+    List<BuildMode> buildModes,
+    List<TargetPlatform> targetPlatforms,
+    bool skipUnknown,
+    bool clobber,
+  }) async {
     final Uri archiveUri = _toStorageUri(version);
-    return _downloadZipArchive('Downloading Material fonts...', archiveUri, location);
+    if (fs.directory(location).listSync().isEmpty || clobber) {
+      await _downloadZipArchive('Downloading Material fonts...', archiveUri, location);
+    }
   }
 }
 
@@ -361,100 +447,387 @@
 class FlutterEngine extends CachedArtifact {
   FlutterEngine(Cache cache) : super('engine', cache);
 
-  List<String> _getPackageDirs() => const <String>['sky_engine'];
-
-  // Return a list of (cache directory path, download URL path) tuples.
-  List<List<String>> _getBinaryDirs() {
-    final List<List<String>> binaryDirs = <List<String>>[];
-
-    binaryDirs.add(<String>['common', 'flutter_patched_sdk.zip']);
-
-    if (cache.includeAllPlatforms)
-      binaryDirs
-        ..addAll(_osxBinaryDirs)
-        ..addAll(_linuxBinaryDirs)
-        ..addAll(_windowsBinaryDirs)
-        ..addAll(_androidBinaryDirs)
-        ..addAll(_iosBinaryDirs)
-        ..addAll(_dartSdks);
-    else if (platform.isLinux)
-      binaryDirs
-        ..addAll(_linuxBinaryDirs)
-        ..addAll(_androidBinaryDirs);
-    else if (platform.isMacOS)
-      binaryDirs
-        ..addAll(_osxBinaryDirs)
-        ..addAll(_androidBinaryDirs)
-        ..addAll(_iosBinaryDirs);
-    else if (platform.isWindows)
-      binaryDirs
-        ..addAll(_windowsBinaryDirs)
-        ..addAll(_androidBinaryDirs);
-
-    return binaryDirs;
+  // Return a list of [BinaryArtifact]s to download.
+  @visibleForTesting
+  List<BinaryArtifact> getBinaryDirs({
+    @required List<BuildMode> buildModes,
+    @required List<TargetPlatform> targetPlatforms,
+    @required bool skipUnknown,
+  }) {
+    TargetPlatform hostPlatform;
+    if (cache.includeAllPlatforms) {
+      hostPlatform = null;
+    } if (platform.isMacOS) {
+      hostPlatform = TargetPlatform.darwin_x64;
+    } else if (platform.isLinux) {
+      hostPlatform = TargetPlatform.linux_x64;
+    } else if (platform.isWindows) {
+      hostPlatform = TargetPlatform.windows_x64;
+    }
+    final List<BinaryArtifact> results = _reduceEngineBinaries(
+      buildModes: buildModes,
+      targetPlatforms: targetPlatforms,
+      hostPlatform: hostPlatform,
+      skipUnknown: skipUnknown,
+    ).toList();
+    if (cache.includeAllPlatforms) {
+      return results + _dartSdks;
+    }
+    return results;
   }
 
-  List<List<String>> get _osxBinaryDirs => <List<String>>[
-    <String>['darwin-x64', 'darwin-x64/artifacts.zip'],
-    <String>['android-arm-profile/darwin-x64', 'android-arm-profile/darwin-x64.zip'],
-    <String>['android-arm-release/darwin-x64', 'android-arm-release/darwin-x64.zip'],
-    <String>['android-arm64-profile/darwin-x64', 'android-arm64-profile/darwin-x64.zip'],
-    <String>['android-arm64-release/darwin-x64', 'android-arm64-release/darwin-x64.zip'],
-    <String>['android-arm-dynamic-profile/darwin-x64', 'android-arm-dynamic-profile/darwin-x64.zip'],
-    <String>['android-arm-dynamic-release/darwin-x64', 'android-arm-dynamic-release/darwin-x64.zip'],
-    <String>['android-arm64-dynamic-profile/darwin-x64', 'android-arm64-dynamic-profile/darwin-x64.zip'],
-    <String>['android-arm64-dynamic-release/darwin-x64', 'android-arm64-dynamic-release/darwin-x64.zip'],
+  Iterable<BinaryArtifact> _reduceEngineBinaries({
+    List<BuildMode> buildModes,
+    List<TargetPlatform> targetPlatforms,
+    TargetPlatform hostPlatform,
+    bool skipUnknown,
+  }) {
+    return _binaries.where((BinaryArtifact engineBinary) {
+      if (hostPlatform != null && engineBinary.hostPlatform != null && engineBinary.hostPlatform != hostPlatform) {
+        return false;
+      }
+      // match if artifact has no restrictions.
+      if (engineBinary.skipChecks || engineBinary.buildMode == null && engineBinary.targetPlatform == null) {
+        return true;
+      }
+      final bool buildModeMatch = buildModes.any((BuildMode buildMode) => buildMode == engineBinary.buildMode);
+      final bool targetPlatformMatch = targetPlatforms.any((TargetPlatform targetPlatform) => targetPlatform == engineBinary.targetPlatform);
+      if (buildModeMatch && targetPlatformMatch  // match if artifact exactly matches requiremnets.
+        || skipUnknown && buildModeMatch && targetPlatforms.isEmpty // match if build mode matches but target platform is unknown.
+        || skipUnknown && targetPlatformMatch && buildModes.isEmpty // match if target platform matches but build mode is null.
+        || !skipUnknown && targetPlatforms.isEmpty && buildModes.isEmpty) { // match if neither are provided but skipUnknown flag is provided.
+          return true;
+      }
+      return false;
+    });
+  }
+
+  List<BinaryArtifact> get _packages => const <BinaryArtifact>[
+    BinaryArtifact(
+      name: 'sky_engine',
+      fileName: 'sky_engine'
+    ),
   ];
 
-  List<List<String>> get _linuxBinaryDirs => <List<String>>[
-    <String>['linux-x64', 'linux-x64/artifacts.zip'],
-    <String>['android-arm-profile/linux-x64', 'android-arm-profile/linux-x64.zip'],
-    <String>['android-arm-release/linux-x64', 'android-arm-release/linux-x64.zip'],
-    <String>['android-arm64-profile/linux-x64', 'android-arm64-profile/linux-x64.zip'],
-    <String>['android-arm64-release/linux-x64', 'android-arm64-release/linux-x64.zip'],
-    <String>['android-arm-dynamic-profile/linux-x64', 'android-arm-dynamic-profile/linux-x64.zip'],
-    <String>['android-arm-dynamic-release/linux-x64', 'android-arm-dynamic-release/linux-x64.zip'],
-    <String>['android-arm64-dynamic-profile/linux-x64', 'android-arm64-dynamic-profile/linux-x64.zip'],
-    <String>['android-arm64-dynamic-release/linux-x64', 'android-arm64-dynamic-release/linux-x64.zip'],
+  /// This lives separately since we only download it when includeAllPlatforms is true.
+  List<BinaryArtifact> get _dartSdks => const <BinaryArtifact>[
+    BinaryArtifact(
+      name: 'darwin-x64',
+      fileName: 'dart-sdk-darwin-x64.zip',
+    ),
+    BinaryArtifact(
+      name: 'linux-x64',
+      fileName: 'dart-sdk-linux-x64.zip',
+    ),
+    BinaryArtifact(
+      name: 'windows-x64',
+      fileName: 'dart-sdk-windows-x64.zip',
+    ),
   ];
 
-  List<List<String>> get _windowsBinaryDirs => <List<String>>[
-    <String>['windows-x64', 'windows-x64/artifacts.zip'],
-    <String>['android-arm-profile/windows-x64', 'android-arm-profile/windows-x64.zip'],
-    <String>['android-arm-release/windows-x64', 'android-arm-release/windows-x64.zip'],
-    <String>['android-arm64-profile/windows-x64', 'android-arm64-profile/windows-x64.zip'],
-    <String>['android-arm64-release/windows-x64', 'android-arm64-release/windows-x64.zip'],
-    <String>['android-arm-dynamic-profile/windows-x64', 'android-arm-dynamic-profile/windows-x64.zip'],
-    <String>['android-arm-dynamic-release/windows-x64', 'android-arm-dynamic-release/windows-x64.zip'],
-    <String>['android-arm64-dynamic-profile/windows-x64', 'android-arm64-dynamic-profile/windows-x64.zip'],
-    <String>['android-arm64-dynamic-release/windows-x64', 'android-arm64-dynamic-release/windows-x64.zip'],
-  ];
-
-  List<List<String>> get _androidBinaryDirs => <List<String>>[
-    <String>['android-x86', 'android-x86/artifacts.zip'],
-    <String>['android-x64', 'android-x64/artifacts.zip'],
-    <String>['android-arm', 'android-arm/artifacts.zip'],
-    <String>['android-arm-profile', 'android-arm-profile/artifacts.zip'],
-    <String>['android-arm-release', 'android-arm-release/artifacts.zip'],
-    <String>['android-arm64', 'android-arm64/artifacts.zip'],
-    <String>['android-arm64-profile', 'android-arm64-profile/artifacts.zip'],
-    <String>['android-arm64-release', 'android-arm64-release/artifacts.zip'],
-    <String>['android-arm-dynamic-profile', 'android-arm-dynamic-profile/artifacts.zip'],
-    <String>['android-arm-dynamic-release', 'android-arm-dynamic-release/artifacts.zip'],
-    <String>['android-arm64-dynamic-profile', 'android-arm64-dynamic-profile/artifacts.zip'],
-    <String>['android-arm64-dynamic-release', 'android-arm64-dynamic-release/artifacts.zip'],
-  ];
-
-  List<List<String>> get _iosBinaryDirs => <List<String>>[
-    <String>['ios', 'ios/artifacts.zip'],
-    <String>['ios-profile', 'ios-profile/artifacts.zip'],
-    <String>['ios-release', 'ios-release/artifacts.zip'],
-  ];
-
-  List<List<String>> get _dartSdks => <List<String>> [
-    <String>['darwin-x64', 'dart-sdk-darwin-x64.zip'],
-    <String>['linux-x64', 'dart-sdk-linux-x64.zip'],
-    <String>['windows-x64', 'dart-sdk-windows-x64.zip'],
+  /// A set of all possible artifacts to download.
+  ///
+  /// Adding a new artifact:
+  ///
+  /// To ensure that we do not waste a user's time/data/storage, the flutter
+  /// tool should only binaries when they are required. These can be requested
+  /// in [FlutterCommand.updateCache].
+  ///
+  /// An artifact should have the following features to prevent unecessary download:
+  ///
+  ///   * `hostPlatform` should be one of `TargetPlatform.linux_x64`,
+  ///   `TargetPlatform.darwin_x64`, or `TargetPlatfrom.windows_x64`. In the
+  ///   case where there is no restriction it can be left as null.
+  ///   * `buildMode` should be one of `BuildMode.debug`, `BuildMode.profile`,
+  ///   `BuildMode.release`, `BuildMode.dynamicRelease`, or
+  ///   `BuildMode.dynamicProfile`. In the case where it is required regardless
+  ///   of buildMode, it can be left null.
+  ///   * `targetPlatform` should be one of the supported target platforms.
+  ///   * If, despite the restrictions above, the artifact should still be
+  ///   downloaded, `skipChecks` can be set to true.
+  List<BinaryArtifact> get _binaries => const <BinaryArtifact>[
+    BinaryArtifact(
+      name: 'common',
+      fileName: 'flutter_patched_sdk.zip',
+    ),
+    BinaryArtifact(
+      name: 'linux-x64',
+      fileName: 'linux-x64/artifacts.zip',
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-profile/linux-x64',
+      fileName: 'android-arm-profile/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.profile,
+      hostPlatform: TargetPlatform.linux_x64,
+      skipChecks: true,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-release/linux-x64',
+      fileName: 'android-arm-release/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.release,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-profile/linux-x64',
+      fileName: 'android-arm64-profile/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.profile,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-release/linux-x64',
+      fileName: 'android-arm64-release/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.release,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-profile/linux-x64',
+      fileName: 'android-arm-dynamic-profile/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.dynamicProfile,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-release/linux-x64',
+      fileName:  'android-arm-dynamic-release/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.dynamicRelease,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-profile/linux-x64',
+      fileName: 'android-arm64-dynamic-profile/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.dynamicProfile,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-release/linux-x64',
+      fileName: 'android-arm64-dynamic-release/linux-x64.zip',
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.dynamicRelease,
+      hostPlatform: TargetPlatform.linux_x64,
+    ),
+    BinaryArtifact(
+      name: 'windows-x64',
+      fileName: 'windows-x64/artifacts.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-profile/windows-x64',
+      fileName: 'android-arm-profile/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.profile,
+      skipChecks: true
+    ),
+    BinaryArtifact(
+      name: 'android-arm-release/windows-x64',
+      fileName: 'android-arm-release/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.release,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-profile/windows-x64',
+      fileName: 'android-arm64-profile/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.profile,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-release/windows-x64',
+      fileName: 'android-arm64-release/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.release,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-profile/windows-x64',
+      fileName: 'android-arm-dynamic-profile/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.dynamicProfile,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-release/windows-x64',
+      fileName: 'android-arm-dynamic-release/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm,
+      buildMode: BuildMode.dynamicRelease,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-profile/windows-x64',
+      fileName: 'android-arm64-dynamic-profile/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.dynamicProfile,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-release/windows-x64',
+      fileName: 'android-arm64-dynamic-release/windows-x64.zip',
+      hostPlatform: TargetPlatform.windows_x64,
+      targetPlatform: TargetPlatform.android_arm64,
+      buildMode: BuildMode.dynamicRelease,
+    ),
+    BinaryArtifact(
+      name: 'android-x86',
+      fileName: 'android-x86/artifacts.zip',
+      buildMode: BuildMode.debug,
+      targetPlatform: TargetPlatform.android_x86,
+    ),
+    BinaryArtifact(
+      name: 'android-x64',
+      fileName: 'android-x64/artifacts.zip',
+      buildMode: BuildMode.debug,
+      targetPlatform: TargetPlatform.android_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm',
+      fileName: 'android-arm/artifacts.zip',
+      buildMode: BuildMode.debug,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-profile',
+      fileName: 'android-arm-profile/artifacts.zip',
+      buildMode: BuildMode.profile,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-release',
+      fileName: 'android-arm-release/artifacts.zip',
+      buildMode: BuildMode.release,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64',
+      fileName: 'android-arm64/artifacts.zip',
+      buildMode: BuildMode.debug,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-profile',
+      fileName: 'android-arm64-profile/artifacts.zip',
+      buildMode: BuildMode.profile,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-release',
+      fileName: 'android-arm64-release/artifacts.zip',
+      buildMode: BuildMode.release,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-profile',
+      fileName: 'android-arm-dynamic-profile/artifacts.zip',
+      buildMode: BuildMode.dynamicProfile,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-release',
+      fileName: 'android-arm-dynamic-release/artifacts.zip',
+      buildMode: BuildMode.dynamicRelease,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-profile',
+      fileName: 'android-arm64-dynamic-profile/artifacts.zip',
+      buildMode: BuildMode.dynamicProfile,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-release',
+      fileName: 'android-arm64-dynamic-release/artifacts.zip',
+      buildMode: BuildMode.dynamicRelease,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'ios', fileName: 'ios/artifacts.zip',
+      buildMode: BuildMode.debug,
+      hostPlatform: TargetPlatform.darwin_x64,
+      targetPlatform: TargetPlatform.ios,
+    ),
+    BinaryArtifact(
+      name: 'ios-profile',
+      fileName: 'ios-profile/artifacts.zip',
+      buildMode: BuildMode.profile,
+      hostPlatform: TargetPlatform.darwin_x64,
+      targetPlatform: TargetPlatform.ios,
+    ),
+    BinaryArtifact(
+      name: 'ios-release',
+      fileName: 'ios-release/artifacts.zip',
+      buildMode: BuildMode.release,
+      hostPlatform: TargetPlatform.darwin_x64,
+      targetPlatform: TargetPlatform.ios,
+    ),
+    BinaryArtifact(
+      name: 'darwin-x64',
+      fileName: 'darwin-x64/artifacts.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-profile/darwin-x64',
+      fileName: 'android-arm-profile/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.profile,
+      targetPlatform: TargetPlatform.android_arm,
+      skipChecks: true,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-release/darwin-x64',
+      fileName: 'android-arm-release/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.release,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-profile/darwin-x64',
+      fileName: 'android-arm64-profile/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.profile,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-release/darwin-x64',
+      fileName: 'android-arm64-release/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.release,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-profile/darwin-x64',
+      fileName: 'android-arm-dynamic-profile/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.dynamicProfile,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm-dynamic-release/darwin-x64',
+      fileName: 'android-arm-dynamic-release/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.dynamicRelease,
+      targetPlatform: TargetPlatform.android_arm,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-profile/darwin-x64',
+      fileName: 'android-arm64-dynamic-profile/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.dynamicProfile,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
+    BinaryArtifact(
+      name: 'android-arm64-dynamic-release/darwin-x64',
+      fileName: 'android-arm64-dynamic-release/darwin-x64.zip',
+      hostPlatform: TargetPlatform.darwin_x64,
+      buildMode: BuildMode.dynamicRelease,
+      targetPlatform: TargetPlatform.android_arm64,
+    ),
   ];
 
   // A list of cache directory paths to which the LICENSE file should be copied.
@@ -466,52 +839,71 @@
   }
 
   @override
-  bool isUpToDateInner() {
+  UpdateResult isUpToDate({
+    List<BuildMode> buildModes = const <BuildMode>[],
+    List<TargetPlatform> targetPlatforms = const <TargetPlatform>[],
+    bool skipUnknown = true,
+  }) {
+    final UpdateResult parentResult = super.isUpToDate(
+      buildModes: buildModes,
+      targetPlatforms: targetPlatforms,
+      skipUnknown: skipUnknown
+    );
+    if (!parentResult.isUpToDate || parentResult.clobber) {
+      return parentResult;
+    }
     final Directory pkgDir = cache.getCacheDir('pkg');
-    for (String pkgName in _getPackageDirs()) {
-      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
-      if (!fs.directory(pkgPath).existsSync())
-        return false;
+    for (BinaryArtifact packageArtifact in _packages) {
+      final Directory packageDirectory = packageArtifact.artifactLocation(pkgDir);
+      if (!packageDirectory.existsSync()) {
+        return const UpdateResult(isUpToDate: false);
+      }
     }
-
-    for (List<String> toolsDir in _getBinaryDirs()) {
-      final Directory dir = fs.directory(fs.path.join(location.path, toolsDir[0]));
-      if (!dir.existsSync())
-        return false;
+    for (BinaryArtifact toolsArtifact in getBinaryDirs(buildModes: buildModes, targetPlatforms: targetPlatforms, skipUnknown: skipUnknown)) {
+      final Directory dir = toolsArtifact.artifactLocation(location);
+      if (!dir.existsSync()) {
+        return const UpdateResult(isUpToDate: false);
+      }
     }
-
-    for (String licenseDir in _getLicenseDirs()) {
-      final File file = fs.file(fs.path.join(location.path, licenseDir, 'LICENSE'));
-      if (!file.existsSync())
-        return false;
-    }
-    return true;
+    return const UpdateResult(isUpToDate: true);
   }
 
   @override
-  Future<void> updateInner() async {
+  Future<void> updateInner({
+    @required List<BuildMode> buildModes,
+    @required List<TargetPlatform> targetPlatforms,
+    @required bool skipUnknown,
+    @required bool clobber,
+  }) async {
     final String url = '$_storageBaseUrl/flutter_infra/flutter/$version/';
-
-    final Directory pkgDir = cache.getCacheDir('pkg');
-    for (String pkgName in _getPackageDirs()) {
-      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
+    final Directory packageDirectory = cache.getCacheDir('pkg');
+    for (BinaryArtifact rawArtifact in _packages) {
+      final String pkgPath = fs.path.join(packageDirectory.path, rawArtifact.name);
       final Directory dir = fs.directory(pkgPath);
-      if (dir.existsSync())
+      final bool exists = dir.existsSync();
+      if (exists) {
+        if (!clobber) {
+          continue;
+        }
         dir.deleteSync(recursive: true);
-      await _downloadZipArchive('Downloading package $pkgName...', Uri.parse(url + pkgName + '.zip'), pkgDir);
+      }
+      final Uri uri = Uri.parse('$url${rawArtifact.name}.zip');
+      await _downloadZipArchive('Downloading package ${rawArtifact.name}...', uri, packageDirectory);
     }
 
-    for (List<String> toolsDir in _getBinaryDirs()) {
-      final String cacheDir = toolsDir[0];
-      final String urlPath = toolsDir[1];
-      final Directory dir = fs.directory(fs.path.join(location.path, cacheDir));
-      await _downloadZipArchive('Downloading $cacheDir tools...', Uri.parse(url + urlPath), dir);
+    final List<BinaryArtifact> rawArtifacts = getBinaryDirs(buildModes: buildModes, targetPlatforms: targetPlatforms, skipUnknown: skipUnknown);
+    for (BinaryArtifact rawArtifact in rawArtifacts) {
+      final Directory artifactDirectory = rawArtifact.artifactLocation(location);
+      if (artifactDirectory.existsSync() && !clobber) {
+        continue;
+      }
+      final Uri uri = rawArtifact.artifactRemoteLocation(url);
+      await _downloadZipArchive('Downloading ${rawArtifact.name} tools...', uri, artifactDirectory);
+      _makeFilesExecutable(artifactDirectory);
 
-      _makeFilesExecutable(dir);
-
-      final File frameworkZip = fs.file(fs.path.join(dir.path, 'Flutter.framework.zip'));
+      final File frameworkZip = fs.file(fs.path.join(artifactDirectory.path, 'Flutter.framework.zip'));
       if (frameworkZip.existsSync()) {
-        final Directory framework = fs.directory(fs.path.join(dir.path, 'Flutter.framework'));
+        final Directory framework = fs.directory(fs.path.join(artifactDirectory.path, 'Flutter.framework'));
         framework.createSync();
         os.unzip(frameworkZip, framework);
       }
@@ -520,45 +912,43 @@
     final File licenseSource = fs.file(fs.path.join(Cache.flutterRoot, 'LICENSE'));
     for (String licenseDir in _getLicenseDirs()) {
       final String licenseDestinationPath = fs.path.join(location.path, licenseDir, 'LICENSE');
+      // If the destination does not exist, we did not download the artifact to
+      // perform this operation.
+      if (!fs.directory(fs.path.join(location.path, licenseDir)).existsSync()) {
+        continue;
+      }
       await licenseSource.copy(licenseDestinationPath);
     }
   }
 
+  // Checks whether the remote artifacts for `engineVersion` are availible in storage.
   Future<bool> areRemoteArtifactsAvailable({
     String engineVersion,
     bool includeAllPlatforms = true,
   }) async {
-    final bool includeAllPlatformsState = cache.includeAllPlatforms;
+    final bool includeAllPlatforms = cache.includeAllPlatforms;
     cache.includeAllPlatforms = includeAllPlatforms;
-
-    Future<bool> checkForArtifacts(String engineVersion) async {
-      engineVersion ??= version;
-      final String url = '$_storageBaseUrl/flutter_infra/flutter/$engineVersion/';
-
-      bool exists = false;
-      for (String pkgName in _getPackageDirs()) {
-        exists = await _doesRemoteExist('Checking package $pkgName is available...',
-            Uri.parse(url + pkgName + '.zip'));
+    engineVersion ??= version;
+    final String url = '$_storageBaseUrl/flutter_infra/flutter/$engineVersion/';
+    final bool result = await () async {
+      for (BinaryArtifact packageArtifact in _packages) {
+        final Uri uri = Uri.parse('$url${packageArtifact.name}.zip');
+        final bool exists = await _doesRemoteExist('Checking package ${packageArtifact.name} is available...', uri);
         if (!exists) {
           return false;
         }
       }
-
-      for (List<String> toolsDir in _getBinaryDirs()) {
-        final String cacheDir = toolsDir[0];
-        final String urlPath = toolsDir[1];
-        exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
-            Uri.parse(url + urlPath));
+      final List<BinaryArtifact> rawArtifacts = getBinaryDirs(buildModes: <BuildMode>[], targetPlatforms: <TargetPlatform>[], skipUnknown: false);
+      for (BinaryArtifact rawArtifact in rawArtifacts) {
+        final Uri uri = Uri.parse('$url${rawArtifact.fileName}');
+        final bool exists = await _doesRemoteExist('Checking ${rawArtifact.name} tools are available...', uri);
         if (!exists) {
           return false;
         }
       }
-
       return true;
-    }
-
-    final bool result = await checkForArtifacts(engineVersion);
-    cache.includeAllPlatforms = includeAllPlatformsState;
+    }();
+    cache.includeAllPlatforms = includeAllPlatforms;
     return result;
   }
 
@@ -567,8 +957,9 @@
     for (FileSystemEntity entity in dir.listSync()) {
       if (entity is File) {
         final String name = fs.path.basename(entity.path);
-        if (name == 'flutter_tester')
+        if (name == 'flutter_tester') {
           os.makeExecutable(entity);
+        }
       }
     }
   }
@@ -583,30 +974,44 @@
   String get _gradleWrapper => fs.path.join('gradle', 'wrapper', 'gradle-wrapper.jar');
 
   @override
-  Future<void> updateInner() {
+  Future<void> updateInner({List<BuildMode> buildModes, List<TargetPlatform> targetPlatforms, bool skipUnknown, bool clobber}) async {
     final Uri archiveUri = _toStorageUri(version);
-    return _downloadZippedTarball('Downloading Gradle Wrapper...', archiveUri, location).then<void>((_) {
-      // Delete property file, allowing templates to provide it.
-      fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
-      // Remove NOTICE file. Should not be part of the template.
-      fs.file(fs.path.join(location.path, 'NOTICE')).deleteSync();
-    });
+    if (fs.directory(location).listSync().isEmpty || clobber) {
+      await _downloadZippedTarball('Downloading Gradle Wrapper...', archiveUri, location).then<void>((_) {
+        // Delete property file, allowing templates to provide it.
+        fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
+        // Remove NOTICE file. Should not be part of the template.
+        fs.file(fs.path.join(location.path, 'NOTICE')).deleteSync();
+      });
+    }
   }
 
   @override
-  bool isUpToDateInner() {
+  UpdateResult isUpToDate({
+    List<BuildMode> buildModes = const <BuildMode>[],
+    List<TargetPlatform> targetPlatforms = const <TargetPlatform>[],
+    bool skipUnknown = true,
+  }) {
+    final UpdateResult parentResult = super.isUpToDate(
+      buildModes: buildModes,
+      targetPlatforms: targetPlatforms,
+      skipUnknown: skipUnknown,
+    );
+    if (!parentResult.isUpToDate || parentResult.clobber) {
+      return parentResult;
+    }
     final Directory wrapperDir = cache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'));
     if (!fs.directory(wrapperDir).existsSync())
-      return false;
+      return const UpdateResult(isUpToDate: false);
     for (String scriptName in _gradleScripts) {
       final File scriptFile = fs.file(fs.path.join(wrapperDir.path, scriptName));
       if (!scriptFile.existsSync())
-        return false;
+        return const UpdateResult(isUpToDate: false);
     }
     final File gradleWrapperJar = fs.file(fs.path.join(wrapperDir.path, _gradleWrapper));
     if (!gradleWrapperJar.existsSync())
-      return false;
-    return true;
+      return const UpdateResult(isUpToDate: false);
+    return const UpdateResult(isUpToDate: true);
   }
 }
 
@@ -661,3 +1066,34 @@
   if (!directory.existsSync())
     directory.createSync(recursive: true);
 }
+
+class BinaryArtifact {
+  const BinaryArtifact({
+    this.targetPlatform,
+    this.buildMode,
+    @required this.name,
+    @required this.fileName,
+    this.hostPlatform,
+    this.skipChecks = false,
+  });
+
+  final TargetPlatform targetPlatform;
+  final TargetPlatform hostPlatform;
+  final BuildMode buildMode;
+  final String name;
+  final String fileName;
+  final bool skipChecks;
+
+  /// The location where this artifact will be cached.
+  Directory artifactLocation(Directory location) {
+    return fs.directory(fs.path.join(location.path, name));
+  }
+
+  /// The remote location where this artifact can be downloaded.
+  Uri artifactRemoteLocation(String baseUrl) {
+    return Uri.parse('$baseUrl$fileName');
+  }
+
+  @override
+  String toString() => '$name/$fileName';
+}
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index b90e332..8205c72 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -49,9 +49,15 @@
   final String description = 'Build an iOS application bundle (Mac OS X host only).';
 
   @override
-  Future<FlutterCommandResult> runCommand() async {
-    final bool forSimulator = argResults['simulator'];
+  Future<void> validateCommand() async {
     defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
+    return super.validateCommand();
+  }
+
+  bool get forSimulator => argResults['simulator'];
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
 
     if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
       throwToolExit('Building for iOS is only supported on the Mac.');
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index e3d1d0e..b786415 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -212,8 +212,6 @@
       throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
         'variable was specified. Unable to find package:flutter.', exitCode: 2);
 
-    await Cache.instance.updateAll();
-
     final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
 
     final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages');
diff --git a/packages/flutter_tools/lib/src/commands/ide_config.dart b/packages/flutter_tools/lib/src/commands/ide_config.dart
index 2e14108..eaa4dd9 100644
--- a/packages/flutter_tools/lib/src/commands/ide_config.dart
+++ b/packages/flutter_tools/lib/src/commands/ide_config.dart
@@ -223,8 +223,6 @@
       throwToolExit('Currently, the only supported IDE is IntelliJ\n$usage', exitCode: 2);
     }
 
-    await Cache.instance.updateAll();
-
     if (argResults['update-templates']) {
       _handleTemplateUpdate();
       return null;
diff --git a/packages/flutter_tools/lib/src/commands/precache.dart b/packages/flutter_tools/lib/src/commands/precache.dart
index b1fc4dc..7137937 100644
--- a/packages/flutter_tools/lib/src/commands/precache.dart
+++ b/packages/flutter_tools/lib/src/commands/precache.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import '../cache.dart';
 import '../globals.dart';
 import '../runner/flutter_command.dart';
 
@@ -11,6 +12,8 @@
   PrecacheCommand() {
     argParser.addFlag('all-platforms', abbr: 'a', negatable: false,
         help: 'Precache artifacts for all platforms.');
+    argParser.addFlag('force', abbr: 'f', negatable: false,
+        help: 'Force download of new cached artifacts');
   }
 
   @override
@@ -24,14 +27,18 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
-    if (argResults['all-platforms'])
+    if (argResults['all-platforms']) {
       cache.includeAllPlatforms = true;
-
-    if (cache.isUpToDate())
+    }
+    final UpdateResult result = cache.isUpToDate(skipUnknown: false);
+    if (result.isUpToDate && !result.clobber && !argResults['force']) {
       printStatus('Already up-to-date.');
-    else
-      await cache.updateAll();
-
-    return null;
+    } else {
+      await cache.updateAll(
+        skipUnknown: false,
+        clobber: argResults['force'] || result.clobber,
+      );
+    }
+    return const FlutterCommandResult(ExitStatus.success);
   }
 }
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 2d93bfb..b90cdac 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -327,30 +327,43 @@
   }
 
   BuildMode getBuildMode() {
-    final List<bool> modeFlags = <bool>[argResults['debug'], argResults['profile'], argResults['release']];
-    if (modeFlags.where((bool flag) => flag).length > 1)
+    bool debug;
+    bool profile;
+    bool release;
+    if (argParser.options.containsKey('debug')) {
+      debug = argResults['debug'];
+    } else {
+      debug = _defaultBuildMode == BuildMode.debug;
+    }
+    if (argParser.options.containsKey('profile')) {
+      profile = argResults['profile'];
+    } else {
+      profile = _defaultBuildMode == BuildMode.profile;
+    }
+    if (argParser.options.containsKey('release')) {
+      release = argResults['release'];
+    } else {
+      release = _defaultBuildMode == BuildMode.release;
+    }
+    if (debug && profile || debug && release || release && profile) {
       throw UsageException('Only one of --debug, --profile, or --release can be specified.', null);
+    }
     final bool dynamicFlag = argParser.options.containsKey('dynamic')
         ? argResults['dynamic']
         : false;
 
-    if (argResults['debug']) {
-      if (dynamicFlag)
+    if (debug) {
+      if (dynamicFlag) {
         throw ToolExit('Error: --dynamic requires --release or --profile.');
+      }
       return BuildMode.debug;
     }
-    if (argResults['profile'])
+    if (profile) {
       return dynamicFlag ? BuildMode.dynamicProfile : BuildMode.profile;
-    if (argResults['release'])
+    }
+    if (release) {
       return dynamicFlag ? BuildMode.dynamicRelease : BuildMode.release;
-
-    if (_defaultBuildMode == BuildMode.debug && dynamicFlag)
-      throw ToolExit('Error: --dynamic requires --release or --profile.');
-    if (_defaultBuildMode == BuildMode.release && dynamicFlag)
-      return BuildMode.dynamicRelease;
-    if (_defaultBuildMode == BuildMode.profile && dynamicFlag)
-      return BuildMode.dynamicProfile;
-
+    }
     return _defaultBuildMode;
   }
 
@@ -388,7 +401,7 @@
           '--patch-number (${argResults['patch-number']}) must be an int.', null);
     }
 
-    String extraFrontEndOptions =
+    List<String> extraFrontEndOptions =
         argParser.options.containsKey(FlutterOptions.kExtraFrontEndOptions)
             ? argResults[FlutterOptions.kExtraFrontEndOptions]
             : null;
@@ -397,9 +410,9 @@
       for (String expFlag in argResults[FlutterOptions.kEnableExperiment]) {
         final String flag = '--enable-experiment=' + expFlag;
         if (extraFrontEndOptions != null) {
-          extraFrontEndOptions += ',' + flag;
+          extraFrontEndOptions.add(flag);
         } else {
-          extraFrontEndOptions = flag;
+          extraFrontEndOptions = <String>[flag];
         }
       }
     }
@@ -425,9 +438,9 @@
       baselineDir: argParser.options.containsKey('baseline-dir')
           ? argResults['baseline-dir']
           : null,
-      extraFrontEndOptions: extraFrontEndOptions,
+      extraFrontEndOptions: extraFrontEndOptions?.join(', '),
       extraGenSnapshotOptions: argParser.options.containsKey(FlutterOptions.kExtraGenSnapshotOptions)
-          ? argResults[FlutterOptions.kExtraGenSnapshotOptions]
+          ? argResults[FlutterOptions.kExtraGenSnapshotOptions]?.join(', ')
           : null,
       buildSharedLibrary: argParser.options.containsKey('build-shared-library')
         ? argResults['build-shared-library']
@@ -517,6 +530,33 @@
     );
   }
 
+  /// A hook called to populate the cache with a particular target platform
+  /// or build mode.
+  ///
+  /// If a command requires specific artifacts, it is it's responsibility to
+  /// request them here.
+  Future<void> updateCache() async {
+    // Download all artifacts unless told otherwise.
+    await cache.updateAll(
+      buildModes: <BuildMode>[
+        BuildMode.debug,
+        BuildMode.release,
+        BuildMode.profile,
+        BuildMode.dynamicProfile,
+        BuildMode.dynamicRelease,
+      ],
+      targetPlatforms: <TargetPlatform>[
+        TargetPlatform.android_arm,
+        TargetPlatform.android_arm64,
+        TargetPlatform.android_x64,
+        TargetPlatform.android_x86,
+        TargetPlatform.ios,
+      ],
+      clobber: false,
+      skipUnknown: true,
+    );
+  }
+
   /// Perform validation then call [runCommand] to execute the command.
   /// Return a [Future] that completes with an exit code
   /// indicating whether execution was successful.
@@ -527,11 +567,11 @@
   @mustCallSuper
   Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
     await validateCommand();
-
     // 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();
+    if (shouldUpdateCache) {
+      await updateCache();
+    }
 
     if (shouldRunPub) {
       await pubGet(context: PubContext.getVerifyContext(name));
diff --git a/packages/flutter_tools/test/cache_test.dart b/packages/flutter_tools/test/cache_test.dart
index 1e7916f..8b25990 100644
--- a/packages/flutter_tools/test/cache_test.dart
+++ b/packages/flutter_tools/test/cache_test.dart
@@ -6,6 +6,7 @@
 
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 
@@ -60,7 +61,8 @@
       directory.createSync(recursive: true);
       fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradle', 'wrapper', 'gradle-wrapper.jar')).createSync(recursive: true);
       when(mockCache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'))).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
-      expect(gradleWrapper.isUpToDateInner(), false);
+      when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
+      expect(gradleWrapper.isUpToDate().isUpToDate, const UpdateResult(isUpToDate: false).isUpToDate);
     }, overrides: <Type, Generator>{
       Cache: ()=> mockCache,
       FileSystem: () => fs,
@@ -75,7 +77,8 @@
       fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradlew.bat')).createSync(recursive: true);
 
       when(mockCache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'))).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
-      expect(gradleWrapper.isUpToDateInner(), true);
+      when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
+      expect(gradleWrapper.isUpToDate().isUpToDate, const UpdateResult(isUpToDate: true).isUpToDate);
     }, overrides: <Type, Generator>{
       Cache: ()=> mockCache,
       FileSystem: () => fs,
@@ -84,37 +87,37 @@
     test('should not be up to date, if some cached artifact is not', () {
       final CachedArtifact artifact1 = MockCachedArtifact();
       final CachedArtifact artifact2 = MockCachedArtifact();
-      when(artifact1.isUpToDate()).thenReturn(true);
-      when(artifact2.isUpToDate()).thenReturn(false);
+      when(artifact1.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: true, clobber: false));
+      when(artifact2.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: false, clobber: false));
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
-      expect(cache.isUpToDate(), isFalse);
+      expect(cache.isUpToDate().isUpToDate, isFalse);
     });
     test('should be up to date, if all cached artifacts are', () {
       final CachedArtifact artifact1 = MockCachedArtifact();
       final CachedArtifact artifact2 = MockCachedArtifact();
-      when(artifact1.isUpToDate()).thenReturn(true);
-      when(artifact2.isUpToDate()).thenReturn(true);
+      when(artifact1.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: true, clobber: false));
+      when(artifact2.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: true, clobber: false));
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
-      expect(cache.isUpToDate(), isTrue);
+      expect(cache.isUpToDate().isUpToDate, isTrue);
     });
     test('should update cached artifacts which are not up to date', () async {
       final CachedArtifact artifact1 = MockCachedArtifact();
       final CachedArtifact artifact2 = MockCachedArtifact();
-      when(artifact1.isUpToDate()).thenReturn(true);
-      when(artifact2.isUpToDate()).thenReturn(false);
+      when(artifact1.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: true, clobber: false));
+      when(artifact2.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: false, clobber: false));
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
       await cache.updateAll();
-      verifyNever(artifact1.update());
-      verify(artifact2.update());
+      verifyNever(artifact1.update(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown')));
+      verify(artifact2.update(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'), clobber: anyNamed('clobber')));
     });
     testUsingContext('failed storage.googleapis.com download shows China warning', () async {
       final CachedArtifact artifact1 = MockCachedArtifact();
       final CachedArtifact artifact2 = MockCachedArtifact();
-      when(artifact1.isUpToDate()).thenReturn(false);
-      when(artifact2.isUpToDate()).thenReturn(false);
+      when(artifact1.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: false, clobber: false));
+      when(artifact2.isUpToDate(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'))).thenReturn(const UpdateResult(isUpToDate: false, clobber: false));
       final MockInternetAddress address = MockInternetAddress();
       when(address.host).thenReturn('storage.googleapis.com');
-      when(artifact1.update()).thenThrow(SocketException(
+      when(artifact1.update(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'), clobber: anyNamed('clobber'))).thenThrow(SocketException(
         'Connection reset by peer',
         address: address,
       ));
@@ -123,15 +126,203 @@
         await cache.updateAll();
         fail('Mock thrown exception expected');
       } catch (e) {
-        verify(artifact1.update());
+        verify(artifact1.update(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'), clobber: anyNamed('clobber')));
         // Don't continue when retrieval fails.
-        verifyNever(artifact2.update());
+        verifyNever(artifact2.update(buildModes: anyNamed('buildModes'), targetPlatforms: anyNamed('targetPlatforms'), skipUnknown: anyNamed('skipUnknown'), clobber: anyNamed('clobber')));
         expect(
           testLogger.errorText,
           contains('https://flutter.io/community/china'),
         );
       }
     });
+
+    final MockPlatform macos = MockPlatform();
+    final MockPlatform windows = MockPlatform();
+    final MockPlatform linux = MockPlatform();
+    when(macos.isMacOS).thenReturn(true);
+    when(macos.isLinux).thenReturn(false);
+    when(macos.isWindows).thenReturn(false);
+    when(windows.isMacOS).thenReturn(false);
+    when(windows.isLinux).thenReturn(false);
+    when(windows.isWindows).thenReturn(true);
+    when(linux.isMacOS).thenReturn(false);
+    when(linux.isLinux).thenReturn(true);
+    when(linux.isWindows).thenReturn(false);
+
+    testUsingContext('Engine cache filtering - macOS', () {
+      final FlutterEngine flutterEngine = FlutterEngine(MockCache());
+      expect(flutterEngine.getBinaryDirs(
+        buildModes: <BuildMode>[BuildMode.release],
+        targetPlatforms: <TargetPlatform>[TargetPlatform.android_arm],
+        skipUnknown: true,
+      ), unorderedEquals(const <BinaryArtifact>[
+        BinaryArtifact(
+          name: 'common',
+          fileName: 'flutter_patched_sdk.zip',
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release',
+          fileName: 'android-arm-release/artifacts.zip',
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-profile/darwin-x64',
+          fileName: 'android-arm-profile/darwin-x64.zip',
+          hostPlatform: TargetPlatform.darwin_x64,
+          buildMode: BuildMode.profile,
+          targetPlatform: TargetPlatform.android_arm,
+          skipChecks: true,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release/darwin-x64',
+          fileName: 'android-arm-release/darwin-x64.zip',
+          hostPlatform: TargetPlatform.darwin_x64,
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'darwin-x64',
+          fileName: 'darwin-x64/artifacts.zip',
+          hostPlatform: TargetPlatform.darwin_x64,
+        ),
+      ]));
+    }, overrides: <Type, Generator>{
+      Platform: () => macos,
+    });
+
+    testUsingContext('Engine cache filtering - unknown mode - macOS', () {
+      final FlutterEngine flutterEngine = FlutterEngine(MockCache());
+      expect(flutterEngine.getBinaryDirs(
+        buildModes: <BuildMode>[],
+        targetPlatforms: <TargetPlatform>[TargetPlatform.ios],
+        skipUnknown: true,
+      ), unorderedEquals(const <BinaryArtifact>[
+        BinaryArtifact(
+          name: 'common',
+          fileName: 'flutter_patched_sdk.zip',
+        ),
+        BinaryArtifact(
+          name: 'android-arm-profile/darwin-x64',
+          fileName: 'android-arm-profile/darwin-x64.zip',
+          hostPlatform: TargetPlatform.darwin_x64,
+          buildMode: BuildMode.profile,
+          targetPlatform: TargetPlatform.android_arm,
+          skipChecks: true,
+        ),
+        BinaryArtifact(
+          name: 'ios', fileName: 'ios/artifacts.zip',
+          buildMode: BuildMode.debug,
+          hostPlatform: TargetPlatform.darwin_x64,
+          targetPlatform: TargetPlatform.ios,
+        ),
+        BinaryArtifact(
+          name: 'ios-profile',
+          fileName: 'ios-profile/artifacts.zip',
+          buildMode: BuildMode.profile,
+          hostPlatform: TargetPlatform.darwin_x64,
+          targetPlatform: TargetPlatform.ios,
+        ),
+        BinaryArtifact(
+          name: 'ios-release',
+          fileName: 'ios-release/artifacts.zip',
+          buildMode: BuildMode.release,
+          hostPlatform: TargetPlatform.darwin_x64,
+          targetPlatform: TargetPlatform.ios,
+        ),
+        BinaryArtifact(
+          name: 'darwin-x64',
+          fileName: 'darwin-x64/artifacts.zip',
+          hostPlatform: TargetPlatform.darwin_x64,
+        ),
+      ]));
+    }, overrides: <Type, Generator>{
+      Platform: () => macos,
+    });
+
+    testUsingContext('Engine cache filtering - Windows', () {
+      final FlutterEngine flutterEngine = FlutterEngine(MockCache());
+      expect(flutterEngine.getBinaryDirs(
+        buildModes: <BuildMode>[BuildMode.release],
+        targetPlatforms: <TargetPlatform>[TargetPlatform.android_arm],
+        skipUnknown: true,
+      ), unorderedEquals(const <BinaryArtifact>[
+          BinaryArtifact(
+          name: 'common',
+          fileName: 'flutter_patched_sdk.zip',
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release',
+          fileName: 'android-arm-release/artifacts.zip',
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-profile/windows-x64',
+          fileName: 'android-arm-profile/windows-x64.zip',
+          hostPlatform: TargetPlatform.windows_x64,
+          buildMode: BuildMode.profile,
+          targetPlatform: TargetPlatform.android_arm,
+          skipChecks: true,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release/windows-x64',
+          fileName: 'android-arm-release/windows-x64.zip',
+          hostPlatform: TargetPlatform.windows_x64,
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'windows-x64',
+          fileName: 'windows-x64/artifacts.zip',
+          hostPlatform: TargetPlatform.windows_x64,
+        ),
+      ]));
+    }, overrides: <Type, Generator>{
+      Platform: () => windows,
+    });
+
+    testUsingContext('Engine cache filtering - linux', () {
+      final FlutterEngine flutterEngine = FlutterEngine(MockCache());
+      expect(flutterEngine.getBinaryDirs(
+        buildModes: <BuildMode>[BuildMode.release],
+        targetPlatforms: <TargetPlatform>[TargetPlatform.android_arm],
+        skipUnknown: true,
+      ), unorderedEquals(const <BinaryArtifact>[
+          BinaryArtifact(
+          name: 'common',
+          fileName: 'flutter_patched_sdk.zip',
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release',
+          fileName: 'android-arm-release/artifacts.zip',
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-profile/linux-x64',
+          fileName: 'android-arm-profile/linux-x64.zip',
+          hostPlatform: TargetPlatform.linux_x64,
+          buildMode: BuildMode.profile,
+          targetPlatform: TargetPlatform.android_arm,
+          skipChecks: true,
+        ),
+        BinaryArtifact(
+          name: 'android-arm-release/linux-x64',
+          fileName: 'android-arm-release/linux-x64.zip',
+          hostPlatform: TargetPlatform.linux_x64,
+          buildMode: BuildMode.release,
+          targetPlatform: TargetPlatform.android_arm,
+        ),
+        BinaryArtifact(
+          name: 'linux-x64',
+          fileName: 'linux-x64/artifacts.zip',
+          hostPlatform: TargetPlatform.linux_x64,
+        ),
+      ]));
+    }, overrides: <Type, Generator>{
+      Platform: () => linux,
+    });
   });
 
   testUsingContext('flattenNameSubdirs', () {
@@ -163,3 +354,4 @@
 class MockCachedArtifact extends Mock implements CachedArtifact {}
 class MockInternetAddress extends Mock implements InternetAddress {}
 class MockCache extends Mock implements Cache {}
+class MockPlatform extends Mock implements Platform {}
diff --git a/packages/flutter_tools/test/runner/flutter_command_test.dart b/packages/flutter_tools/test/runner/flutter_command_test.dart
index f98371a..ea5bfa8 100644
--- a/packages/flutter_tools/test/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/runner/flutter_command_test.dart
@@ -42,7 +42,12 @@
     testUsingContext('honors shouldUpdateCache true', () async {
       final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
       await flutterCommand.run();
-      verify(cache.updateAll()).called(1);
+      verify(cache.updateAll(
+        buildModes: anyNamed('buildModes'),
+        clobber: anyNamed('clobber'),
+        skipUnknown: anyNamed('skipUnknown'),
+        targetPlatforms: anyNamed('targetPlatforms')
+      )).called(1);
     },
     overrides: <Type, Generator>{
       Cache: () => cache,