[tool/ci] Add iOS/macOS and Dart support to `fetch-deps` (#4562)

Adds `fetch-deps` support for:
- iOS/macOS dependencies, using `pod install`
- Dart package dependencies, using `pub get`

To make avoid doing extra work in the Dart dependencies step when using this with `*_platform_tests` CI, also adds flags for all of the other platforms, and adds a flag that allows skipping Dart dependencies for any package that doesn't have an example supporting any requested platform. This means that we can pass, e.g., `--windows --supporting-target-platforms-only` to only fetch Dart packages for packages with examples that will be build during the build-and-drive Windows tests.

Adds this as a new step in every platform tests CI task, and in the standard analyze step, so that we will pre-fetch Dart packages (and for iOS/macOS, pods). This won't yet fully eliminate later network access (see https://github.com/flutter/flutter/issues/131204), but will give us early warning on any major failures, such as pub being entirely unreachable from the bots.
- These are marked as an infrastructure step; we'll have to see if this ends up being confusing in practice. If `pub` resolution fails for legitimate reasons, such as a PR that tries to require a version of a package that doesn't exist or that has conflicts, this will cause a failure that is marked as infra. My assumption is that the much more common case is going to be that it is actually an infra failure.

Fixes https://github.com/flutter/flutter/issues/130280
diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml
index 793ac03..2262e6a 100644
--- a/.ci/targets/analyze.yaml
+++ b/.ci/targets/analyze.yaml
@@ -4,6 +4,10 @@
     infra_step: true # Note infra steps failing prevents "always" from running.
   - name: analyze repo tools
     script: .ci/scripts/analyze_repo_tools.sh
+  - name: download Dart deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps"]
+    infra_step: true
   - name: analyze
     script: script/tool_runner.sh
     # DO NOT change the custom-analysis argument here without changing the Dart repo.
diff --git a/.ci/targets/android_platform_tests.yaml b/.ci/targets/android_platform_tests.yaml
index 7ee1767..eea5063 100644
--- a/.ci/targets/android_platform_tests.yaml
+++ b/.ci/targets/android_platform_tests.yaml
@@ -2,10 +2,10 @@
   - name: prepare tool
     script: .ci/scripts/prepare_tool.sh
     infra_step: true # Note infra steps failing prevents "always" from running.
-  - name: download android deps
+  - name: download Dart and Android deps
     script: script/tool_runner.sh
     infra_step: true
-    args: ["fetch-deps"]
+    args: ["fetch-deps", "--android", "--supporting-target-platforms-only"]
   - name: build examples
     script: script/tool_runner.sh
     args: ["build-examples", "--apk"]
diff --git a/.ci/targets/ios_platform_tests.yaml b/.ci/targets/ios_platform_tests.yaml
index 97b92d3..212c8c9 100644
--- a/.ci/targets/ios_platform_tests.yaml
+++ b/.ci/targets/ios_platform_tests.yaml
@@ -5,6 +5,10 @@
   - name: create simulator
     script: .ci/scripts/create_simulator.sh
     infra_step: true # Note infra steps failing prevents "always" from running.
+  - name: download Dart and iOS deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps", "--ios", "--supporting-target-platforms-only"]
+    infra_step: true
   - name: build examples
     script: script/tool_runner.sh
     args: ["build-examples", "--ios"]
diff --git a/.ci/targets/linux_platform_tests.yaml b/.ci/targets/linux_platform_tests.yaml
index 3455958..6d13b7d 100644
--- a/.ci/targets/linux_platform_tests.yaml
+++ b/.ci/targets/linux_platform_tests.yaml
@@ -3,6 +3,11 @@
     script: .ci/scripts/prepare_tool.sh
   - name: set default apps
     script: .ci/scripts/set_default_linux_apps.sh
+    infra_step: true
+  - name: download Dart deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps", "--linux", "--supporting-target-platforms-only"]
+    infra_step: true
   - name: build examples
     script: script/tool_runner.sh
     args: ["build-examples", "--linux"]
diff --git a/.ci/targets/macos_platform_tests.yaml b/.ci/targets/macos_platform_tests.yaml
index 92b937e..512e1c9 100644
--- a/.ci/targets/macos_platform_tests.yaml
+++ b/.ci/targets/macos_platform_tests.yaml
@@ -2,6 +2,10 @@
   - name: prepare tool
     script: .ci/scripts/prepare_tool.sh
     infra_step: true # Note infra steps failing prevents "always" from running.
+  - name: download Dart and macOS deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps", "--macos", "--supporting-target-platforms-only"]
+    infra_step: true
   - name: build examples
     script: script/tool_runner.sh
     args: ["build-examples", "--macos"]
diff --git a/.ci/targets/web_platform_tests.yaml b/.ci/targets/web_platform_tests.yaml
index acfb9cd..2d28b45 100644
--- a/.ci/targets/web_platform_tests.yaml
+++ b/.ci/targets/web_platform_tests.yaml
@@ -2,6 +2,10 @@
   - name: prepare tool
     script: .ci/scripts/prepare_tool.sh
     infra_step: true # Note infra steps failing prevents "always" from running.
+  - name: download Dart deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps", "--web", "--supporting-target-platforms-only"]
+    infra_step: true
   - name: build examples
     script: script/tool_runner.sh
     args: ["build-examples", "--web"]
diff --git a/.ci/targets/windows_build_and_platform_tests.yaml b/.ci/targets/windows_build_and_platform_tests.yaml
index 4bd73f9..da1dfa8 100644
--- a/.ci/targets/windows_build_and_platform_tests.yaml
+++ b/.ci/targets/windows_build_and_platform_tests.yaml
@@ -2,6 +2,10 @@
   - name: prepare tool
     script: .ci/scripts/prepare_tool.sh
     infra_step: true # Note infra steps failing prevents "always" from running.
+  - name: download Dart deps
+    script: script/tool_runner.sh
+    args: ["fetch-deps", "--windows", "--supporting-target-platforms-only"]
+    infra_step: true
   - name: build examples (Win32)
     script: .ci/scripts/build_examples_win32.sh
   - name: native unit tests (Win32)
diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart
index 728ac11..0f78d39 100644
--- a/script/tool/lib/src/build_examples_command.dart
+++ b/script/tool/lib/src/build_examples_command.dart
@@ -176,9 +176,8 @@
         // supported platforms. For packages, just log and skip any requested
         // platform that a package doesn't have set up.
         if (!isPlugin &&
-            !example.directory
-                .childDirectory(platform.flutterPlatformDirectory)
-                .existsSync()) {
+            !example.appSupportsPlatform(
+                getPlatformByName(platform.pluginPlatform))) {
           print('Skipping ${platform.label} for $packageName; not supported.');
           continue;
         }
@@ -304,11 +303,6 @@
   /// The `flutter build` build type.
   final String flutterBuildType;
 
-  /// The Flutter platform directory name.
-  // In practice, this is the same as the plugin platform key for all platforms.
-  // If that changes, this can be adjusted.
-  String get flutterPlatformDirectory => pluginPlatform;
-
   /// Any extra flags to pass to `flutter build`.
   final List<String> extraBuildFlags;
 }
diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart
index 96224f5..4cf62b2 100644
--- a/script/tool/lib/src/common/core.dart
+++ b/script/tool/lib/src/common/core.dart
@@ -37,6 +37,25 @@
 // ignore: public_member_api_docs
 enum FlutterPlatform { android, ios, linux, macos, web, windows }
 
+const Map<String, FlutterPlatform> _platformByName = <String, FlutterPlatform>{
+  platformAndroid: FlutterPlatform.android,
+  platformIOS: FlutterPlatform.ios,
+  platformLinux: FlutterPlatform.linux,
+  platformMacOS: FlutterPlatform.macos,
+  platformWeb: FlutterPlatform.web,
+  platformWindows: FlutterPlatform.windows,
+};
+
+/// Maps from a platform name (e.g., flag or platform directory) to the
+/// corresponding platform enum.
+FlutterPlatform getPlatformByName(String name) {
+  final FlutterPlatform? platform = _platformByName[name];
+  if (platform == null) {
+    throw ArgumentError('Invalid platform: $name');
+  }
+  return platform;
+}
+
 // Flutter->Dart SDK version mapping. Any time a command fails to look up a
 // corresponding version, this map should be updated.
 final Map<Version, Version> _dartSdkForFlutterSdk = <Version, Version>{
diff --git a/script/tool/lib/src/common/process_runner.dart b/script/tool/lib/src/common/process_runner.dart
index 7556b55..8ac98b8 100644
--- a/script/tool/lib/src/common/process_runner.dart
+++ b/script/tool/lib/src/common/process_runner.dart
@@ -30,12 +30,13 @@
     String executable,
     List<String> args, {
     Directory? workingDir,
+    Map<String, String>? environment,
     bool exitOnError = false,
   }) async {
     print(
         'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}');
     final io.Process process = await io.Process.start(executable, args,
-        workingDirectory: workingDir?.path);
+        workingDirectory: workingDir?.path, environment: environment);
     await Future.wait(<Future<dynamic>>[
       io.stdout.addStream(process.stdout),
       io.stderr.addStream(process.stderr),
@@ -62,14 +63,19 @@
   /// Defaults to `false`
   ///
   /// Returns the [io.ProcessResult] of the [executable].
-  Future<io.ProcessResult> run(String executable, List<String> args,
-      {Directory? workingDir,
-      bool exitOnError = false,
-      bool logOnError = false,
-      Encoding stdoutEncoding = io.systemEncoding,
-      Encoding stderrEncoding = io.systemEncoding}) async {
+  Future<io.ProcessResult> run(
+    String executable,
+    List<String> args, {
+    Directory? workingDir,
+    Map<String, String>? environment,
+    bool exitOnError = false,
+    bool logOnError = false,
+    Encoding stdoutEncoding = io.systemEncoding,
+    Encoding stderrEncoding = io.systemEncoding,
+  }) async {
     final io.ProcessResult result = await io.Process.run(executable, args,
         workingDirectory: workingDir?.path,
+        environment: environment,
         stdoutEncoding: stdoutEncoding,
         stderrEncoding: stderrEncoding);
     if (result.exitCode != 0) {
diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart
index 2a824f5..345ace5 100644
--- a/script/tool/lib/src/common/repository_package.dart
+++ b/script/tool/lib/src/common/repository_package.dart
@@ -92,6 +92,16 @@
     return directory.childDirectory(directoryName);
   }
 
+  /// Returns true if the package is an app that supports [platform].
+  ///
+  /// The "app" prefix on this method is because this currently only works
+  /// for app packages (e.g., examples).
+  // TODO(stuartmorgan): Add support for non-app packages, by parsing the
+  // pubspec for `flutter:platform:` or `platform:` sections.
+  bool appSupportsPlatform(FlutterPlatform platform) {
+    return platformDirectory(platform).existsSync();
+  }
+
   late final Pubspec _parsedPubspec =
       Pubspec.parse(pubspecFile.readAsStringSync());
 
diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart
index ac3e6f4..535ea1e 100644
--- a/script/tool/lib/src/drive_examples_command.dart
+++ b/script/tool/lib/src/drive_examples_command.dart
@@ -254,7 +254,7 @@
     for (final MapEntry<String, List<String>> entry
         in _targetDeviceFlags.entries) {
       final String platform = entry.key;
-      if (example.directory.childDirectory(platform).existsSync()) {
+      if (example.appSupportsPlatform(getPlatformByName(platform))) {
         deviceFlags.addAll(entry.value);
       } else {
         final String exampleName =
diff --git a/script/tool/lib/src/fetch_deps_command.dart b/script/tool/lib/src/fetch_deps_command.dart
index ce70b41..2f763a0 100644
--- a/script/tool/lib/src/fetch_deps_command.dart
+++ b/script/tool/lib/src/fetch_deps_command.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:file/file.dart';
 
 import 'common/core.dart';
 import 'common/gradle.dart';
@@ -10,12 +11,15 @@
 import 'common/plugin_utils.dart';
 import 'common/repository_package.dart';
 
-/// Download dependencies for the following platforms {android}.
+const int _exitPrecacheFailed = 3;
+const int _exitNothingRequested = 4;
+
+/// Download dependencies, both Dart and native.
 ///
 /// Specficially each platform runs:
 ///   Android: 'gradlew dependencies'.
-///   Dart: TBD (flutter/flutter/issues/130279)
-///   iOS: TBD (flutter/flutter/issues/130280)
+///   Dart: 'flutter pub get'.
+///   iOS/macOS: 'pod install'.
 ///
 /// See https://docs.gradle.org/6.4/userguide/core_dependency_management.html#sec:dependency-mgmt-in-gradle.
 class FetchDepsCommand extends PackageLoopingCommand {
@@ -24,25 +28,153 @@
     super.packagesDir, {
     super.processRunner,
     super.platform,
-  });
+  }) {
+    argParser.addFlag(_dartFlag, defaultsTo: true, help: 'Run "pub get"');
+    argParser.addFlag(_supportingTargetPlatformsOnlyFlag,
+        help: 'Restricts "pub get" runs to packages that have at least one '
+            'example supporting at least one of the platform flags passed.\n'
+            'If no platform flags are passed, this will exclude all packages.');
+    argParser.addFlag(platformAndroid,
+        help: 'Run "gradlew dependencies" for Android plugins.\n'
+            'Include packages with Android examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+    argParser.addFlag(platformIOS,
+        help: 'Run "pod install" for iOS plugins.\n'
+            'Include packages with iOS examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+    argParser.addFlag(platformLinux,
+        help: 'Include packages with Linux examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+    argParser.addFlag(platformMacOS,
+        help: 'Run "pod install" for macOS plugins.\n'
+            'Include packages with macOS examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+    argParser.addFlag(platformWeb,
+        help: 'Include packages with Web examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+    argParser.addFlag(platformWindows,
+        help: 'Include packages with Windows examples when used with '
+            '--$_supportingTargetPlatformsOnlyFlag');
+  }
+
+  static const String _dartFlag = 'dart';
+  static const String _supportingTargetPlatformsOnlyFlag =
+      'supporting-target-platforms-only';
+
+  static const Iterable<String> _platforms = <String>[
+    platformAndroid,
+    platformIOS,
+    platformLinux,
+    platformMacOS,
+    platformWeb,
+    platformWindows,
+  ];
 
   @override
   final String name = 'fetch-deps';
 
   @override
-  final String description = 'Fetches dependencies for plugins.\n'
-      'Runs "gradlew dependencies" on Android plugins.\n'
-      'Dart see flutter/flutter/issues/130279\n'
-      'iOS plugins see flutter/flutter/issues/130280\n'
-      '\n'
-      'Requires the examples to have been built at least once before running.';
+  final String description = 'Fetches dependencies for packages';
+
+  @override
+  Future<void> initializeRun() async {
+    // `pod install` requires having the platform artifacts precached. See
+    // https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/bin/podhelper.rb#L47
+    // https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/bin/podhelper.rb#L130
+    if (getBoolArg(platformIOS)) {
+      final int exitCode = await processRunner.runAndStream(
+        flutterCommand,
+        <String>['precache', '--ios'],
+      );
+      if (exitCode != 0) {
+        throw ToolExit(_exitPrecacheFailed);
+      }
+    }
+    if (getBoolArg(platformMacOS)) {
+      final int exitCode = await processRunner.runAndStream(
+        flutterCommand,
+        <String>['precache', '--macos'],
+      );
+      if (exitCode != 0) {
+        throw ToolExit(_exitPrecacheFailed);
+      }
+    }
+  }
 
   @override
   Future<PackageResult> runForPackage(RepositoryPackage package) async {
+    bool fetchedDeps = false;
+    final List<String> skips = <String>[];
+    if (getBoolArg(_dartFlag)) {
+      final bool filterPlatforms =
+          getBoolArg(_supportingTargetPlatformsOnlyFlag);
+      if (!filterPlatforms || _hasExampleSupportingRequestedPlatform(package)) {
+        fetchedDeps = true;
+        if (!await _fetchDartPackages(package)) {
+          // If Dart-level depenendencies fail, fail immediately since the
+          // native dependencies won't be useful.
+          return PackageResult.fail(<String>['Failed to "pub get".']);
+        }
+      } else {
+        skips.add('Skipping Dart dependencies; no examples support requested '
+            'platforms.');
+      }
+    }
+
+    final List<String> errors = <String>[];
+    for (final FlutterPlatform platform in _targetPlatforms) {
+      final PackageResult result;
+      switch (platform) {
+        case FlutterPlatform.android:
+          result = await _fetchAndroidDeps(package);
+          break;
+        case FlutterPlatform.ios:
+          result = await _fetchDarwinDeps(package, platformIOS);
+          break;
+        case FlutterPlatform.macos:
+          result = await _fetchDarwinDeps(package, platformMacOS);
+          break;
+        case FlutterPlatform.linux:
+        case FlutterPlatform.web:
+        case FlutterPlatform.windows:
+          // No native dependency handling yet.
+          result = PackageResult.skip('Nothing to do for $platform.');
+          break;
+      }
+      switch (result.state) {
+        case RunState.succeeded:
+          fetchedDeps = true;
+          break;
+        case RunState.skipped:
+          skips.add(result.details.first);
+          break;
+        case RunState.failed:
+          errors.addAll(result.details);
+          break;
+        case RunState.excluded:
+          throw StateError('Unreachable');
+      }
+    }
+
+    if (errors.isNotEmpty) {
+      return PackageResult.fail(errors);
+    }
+    if (fetchedDeps) {
+      return PackageResult.success();
+    }
+    if (skips.isNotEmpty) {
+      return PackageResult.skip(<String>['', ...skips].join('\n- '));
+    }
+
+    printError('At least one type of dependency must be requested');
+    throw ToolExit(_exitNothingRequested);
+  }
+
+  Future<PackageResult> _fetchAndroidDeps(RepositoryPackage package) async {
     if (!pluginSupportsPlatform(platformAndroid, package,
         requiredMode: PlatformSupport.inline)) {
       return PackageResult.skip(
-          'Plugin does not have an Android implementation.');
+          'Package does not have native Android dependencies.');
     }
 
     for (final RepositoryPackage example in package.getExamples()) {
@@ -63,7 +195,8 @@
 
       final String packageName = package.directory.basename;
 
-      final int exitCode = await gradleProject.runCommand('$packageName:dependencies');
+      final int exitCode =
+          await gradleProject.runCommand('$packageName:dependencies');
       if (exitCode != 0) {
         return PackageResult.fail();
       }
@@ -71,4 +204,78 @@
 
     return PackageResult.success();
   }
+
+  Future<PackageResult> _fetchDarwinDeps(
+      RepositoryPackage package, final String platform) async {
+    if (!pluginSupportsPlatform(platform, package,
+        requiredMode: PlatformSupport.inline)) {
+      // Convert from the flag (lower case ios/macos) to the actual name.
+      final String displayPlatform = platform.replaceFirst('os', 'OS');
+      return PackageResult.skip(
+          'Package does not have native $displayPlatform dependencies.');
+    }
+
+    for (final RepositoryPackage example in package.getExamples()) {
+      final Directory platformDir =
+          example.platformDirectory(getPlatformByName(platform));
+
+      // Running `pod install` requires `flutter pub get` or `flutter build` to
+      // have been run at some point to create the necessary native build files.
+      // See https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/templates/cocoapods/Podfile-macos#L13-L15
+      // and https://github.com/flutter/flutter/blob/fb7a763c640d247d090cbb373e4b3a0459ac171b/packages/flutter_tools/templates/cocoapods/Podfile-ios-swift#L14-L16
+      final File generatedXCConfig = platform == platformMacOS
+          ? platformDir
+              .childDirectory('Flutter')
+              .childDirectory('ephemeral')
+              .childFile('Flutter-Generated.xcconfig')
+          : platformDir
+              .childDirectory('Flutter')
+              .childFile('Generated.xcconfig');
+      if (!generatedXCConfig.existsSync()) {
+        final int exitCode = await processRunner.runAndStream(
+          flutterCommand,
+          <String>['pub', 'get'],
+          workingDir: example.directory,
+        );
+        if (exitCode != 0) {
+          printError('Unable to prepare native project files.');
+          return PackageResult.fail(<String>['Unable to configure project.']);
+        }
+      }
+
+      final int exitCode = await processRunner.runAndStream(
+        'pod',
+        <String>['install'],
+        workingDir: platformDir,
+        environment: <String, String>{
+          'LANG': 'en_US.UTF-8',
+        },
+      );
+      if (exitCode != 0) {
+        printError('Unable to "pod install"');
+        return PackageResult.fail(<String>['Unable to "pod install"']);
+      }
+    }
+
+    return PackageResult.success();
+  }
+
+  Future<bool> _fetchDartPackages(RepositoryPackage package) async {
+    final String command = package.requiresFlutter() ? flutterCommand : 'dart';
+    final int exitCode = await processRunner.runAndStream(
+        command, <String>['pub', 'get'],
+        workingDir: package.directory);
+    return exitCode == 0;
+  }
+
+  bool _hasExampleSupportingRequestedPlatform(RepositoryPackage package) {
+    return package.getExamples().any((RepositoryPackage example) {
+      return _targetPlatforms.any(
+          (FlutterPlatform platform) => example.appSupportsPlatform(platform));
+    });
+  }
+
+  Iterable<FlutterPlatform> get _targetPlatforms => _platforms
+      .where((String platform) => getBoolArg(platform))
+      .map((String platformName) => getPlatformByName(platformName));
 }
diff --git a/script/tool/test/fetch_deps_command_test.dart b/script/tool/test/fetch_deps_command_test.dart
index baff6f6..ab4cd3f 100644
--- a/script/tool/test/fetch_deps_command_test.dart
+++ b/script/tool/test/fetch_deps_command_test.dart
@@ -36,7 +36,142 @@
           CommandRunner<void>('fetch_deps_test', 'Test for $FetchDepsCommand');
       runner.addCommand(command);
     });
+
+    group('dart', () {
+      test('runs pub get', () async {
+        final RepositoryPackage plugin = createFakePlugin(
+            'plugin1', packagesDir, platformSupport: <String, PlatformDetails>{
+          platformIOS: const PlatformDetails(PlatformSupport.inline)
+        });
+
+        final List<String> output =
+            await runCapturingPrint(runner, <String>['fetch-deps']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              plugin.directory.path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('fails if pub get fails', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformIOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        processRunner
+                .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
+            <FakeProcessInfo>[
+          FakeProcessInfo(MockProcess(exitCode: 1)),
+        ];
+
+        Error? commandError;
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps'], errorHandler: (Error e) {
+          commandError = e;
+        });
+
+        expect(commandError, isA<ToolExit>());
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Failed to "pub get"'),
+              ],
+            ));
+      });
+
+      test('skips unsupported packages when any platforms are passed',
+          () async {
+        final RepositoryPackage packageWithBoth = createFakePackage(
+            'supports_both', packagesDir, extraFiles: <String>[
+          'example/linux/placeholder',
+          'example/windows/placeholder'
+        ]);
+        final RepositoryPackage packageWithOne = createFakePackage(
+            'supports_one', packagesDir,
+            extraFiles: <String>['example/linux/placeholder']);
+        createFakePackage('supports_neither', packagesDir);
+
+        await runCapturingPrint(runner, <String>[
+          'fetch-deps',
+          '--linux',
+          '--windows',
+          '--supporting-target-platforms-only'
+        ]);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+              'dart',
+              const <String>['pub', 'get'],
+              packageWithBoth.path,
+            ),
+            ProcessCall(
+              'dart',
+              const <String>['pub', 'get'],
+              packageWithOne.path,
+            ),
+          ]),
+        );
+      });
+    });
+
     group('android', () {
+      test('runs pub get before gradlew dependencies', () async {
+        final RepositoryPackage plugin =
+            createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
+          'example/android/gradlew',
+        ], platformSupport: <String, PlatformDetails>{
+          platformAndroid: const PlatformDetails(PlatformSupport.inline)
+        });
+
+        final Directory androidDir = plugin
+            .getExamples()
+            .first
+            .platformDirectory(FlutterPlatform.android);
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--android']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              plugin.directory.path,
+            ),
+            ProcessCall(
+              androidDir.childFile('gradlew').path,
+              const <String>['plugin1:dependencies'],
+              androidDir.path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
       test('runs gradlew dependencies', () async {
         final RepositoryPackage plugin =
             createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
@@ -50,8 +185,8 @@
             .first
             .platformDirectory(FlutterPlatform.android);
 
-        final List<String> output =
-            await runCapturingPrint(runner, <String>['fetch-deps']);
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--android']);
 
         expect(
           processRunner.recordedCalls,
@@ -89,8 +224,8 @@
             (RepositoryPackage example) =>
                 example.platformDirectory(FlutterPlatform.android));
 
-        final List<String> output =
-            await runCapturingPrint(runner, <String>['fetch-deps']);
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--android']);
 
         expect(
           processRunner.recordedCalls,
@@ -123,8 +258,8 @@
             .first
             .platformDirectory(FlutterPlatform.android);
 
-        final List<String> output =
-            await runCapturingPrint(runner, <String>['fetch-deps']);
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--android']);
 
         expect(
           processRunner.recordedCalls,
@@ -164,7 +299,8 @@
 
         Error? commandError;
         final List<String> output = await runCapturingPrint(
-            runner, <String>['fetch-deps'], errorHandler: (Error e) {
+            runner, <String>['fetch-deps', '--no-dart', '--android'],
+            errorHandler: (Error e) {
           commandError = e;
         });
 
@@ -199,7 +335,8 @@
 
         Error? commandError;
         final List<String> output = await runCapturingPrint(
-            runner, <String>['fetch-deps'], errorHandler: (Error e) {
+            runner, <String>['fetch-deps', '--no-dart', '--android'],
+            errorHandler: (Error e) {
           commandError = e;
         });
 
@@ -212,41 +349,479 @@
               ],
             ));
       });
+
+      test('skips non-Android plugins', () async {
+        createFakePlugin('plugin1', packagesDir);
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--android']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native Android dependencies.')
+              ],
+            ));
+      });
+
+      test('skips non-inline plugins', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformAndroid: const PlatformDetails(PlatformSupport.federated)
+            });
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--android']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native Android dependencies.')
+              ],
+            ));
+      });
     });
 
-    test('skips non-Android plugins', () async {
-      createFakePlugin('plugin1', packagesDir);
+    group('ios', () {
+      test('runs pub get before pod install', () async {
+        final RepositoryPackage plugin =
+            createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
+          'example/ios/Flutter/Generated.xcconfig',
+        ], platformSupport: <String, PlatformDetails>{
+          platformIOS: const PlatformDetails(PlatformSupport.inline)
+        });
 
-      final List<String> output =
-          await runCapturingPrint(runner, <String>['fetch-deps']);
+        final Directory iOSDir =
+            plugin.getExamples().first.platformDirectory(FlutterPlatform.ios);
 
-      expect(
-          output,
-          containsAllInOrder(
-            <Matcher>[
-              contains(
-                  'SKIPPING: Plugin does not have an Android implementation.')
+        final List<String> output =
+            await runCapturingPrint(runner, <String>['fetch-deps', '--ios']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--ios'],
+              null,
+            ),
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              plugin.directory.path,
+            ),
+            ProcessCall(
+              'pod',
+              const <String>['install'],
+              iOSDir.path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('runs on all examples', () async {
+        final List<String> examples = <String>['example1', 'example2'];
+        final RepositoryPackage plugin = createFakePlugin(
+            'plugin1', packagesDir,
+            examples: examples,
+            extraFiles: <String>[
+              'example/example1/ios/Flutter/Generated.xcconfig',
+              'example/example2/ios/Flutter/Generated.xcconfig',
             ],
-          ));
+            platformSupport: <String, PlatformDetails>{
+              platformIOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        final Iterable<Directory> exampleIOSDirs = plugin.getExamples().map(
+            (RepositoryPackage example) =>
+                example.platformDirectory(FlutterPlatform.ios));
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--ios'],
+              null,
+            ),
+            for (final Directory directory in exampleIOSDirs)
+              ProcessCall(
+                'pod',
+                const <String>['install'],
+                directory.path,
+              ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('runs pub get if example is not configured', () async {
+        final RepositoryPackage plugin = createFakePlugin(
+            'plugin1', packagesDir, platformSupport: <String, PlatformDetails>{
+          platformIOS: const PlatformDetails(PlatformSupport.inline)
+        });
+
+        final RepositoryPackage example = plugin.getExamples().first;
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--ios'],
+              null,
+            ),
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              example.directory.path,
+            ),
+            ProcessCall(
+              'pod',
+              const <String>['install'],
+              example.platformDirectory(FlutterPlatform.ios).path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('fails if pre-pod pub get fails', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformIOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        processRunner
+                .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
+            <FakeProcessInfo>[
+          FakeProcessInfo(MockProcess(), <String>['precache']),
+          FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']),
+        ];
+
+        Error? commandError;
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios'],
+            errorHandler: (Error e) {
+          commandError = e;
+        });
+
+        expect(commandError, isA<ToolExit>());
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Unable to configure project'),
+              ],
+            ));
+      });
+
+      test('fails if pod install fails', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformIOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
+          FakeProcessInfo(MockProcess(exitCode: 1)),
+        ];
+
+        Error? commandError;
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios'],
+            errorHandler: (Error e) {
+          commandError = e;
+        });
+
+        expect(commandError, isA<ToolExit>());
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('The following packages had errors:'),
+              ],
+            ));
+      });
+
+      test('skips non-iOS plugins', () async {
+        createFakePlugin('plugin1', packagesDir);
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native iOS dependencies.')
+              ],
+            ));
+      });
+
+      test('skips non-inline plugins', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformIOS: const PlatformDetails(PlatformSupport.federated)
+            });
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--ios']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native iOS dependencies.')
+              ],
+            ));
+      });
     });
 
-    test('skips non-inline plugins', () async {
-      createFakePlugin('plugin1', packagesDir,
-          platformSupport: <String, PlatformDetails>{
-            platformAndroid: const PlatformDetails(PlatformSupport.federated)
-          });
+    group('macos', () {
+      test('runs pub get before pod install', () async {
+        final RepositoryPackage plugin =
+            createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
+          'example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig',
+        ], platformSupport: <String, PlatformDetails>{
+          platformMacOS: const PlatformDetails(PlatformSupport.inline)
+        });
 
-      final List<String> output =
-          await runCapturingPrint(runner, <String>['fetch-deps']);
+        final Directory macOSDir =
+            plugin.getExamples().first.platformDirectory(FlutterPlatform.macos);
 
-      expect(
-          output,
-          containsAllInOrder(
-            <Matcher>[
-              contains(
-                  'SKIPPING: Plugin does not have an Android implementation.')
+        final List<String> output =
+            await runCapturingPrint(runner, <String>['fetch-deps', '--macos']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--macos'],
+              null,
+            ),
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              plugin.directory.path,
+            ),
+            ProcessCall(
+              'pod',
+              const <String>['install'],
+              macOSDir.path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('runs on all examples', () async {
+        final List<String> examples = <String>['example1', 'example2'];
+        final RepositoryPackage plugin = createFakePlugin(
+            'plugin1', packagesDir,
+            examples: examples,
+            extraFiles: <String>[
+              'example/example1/macos/Flutter/ephemeral/Flutter-Generated.xcconfig',
+              'example/example2/macos/Flutter/ephemeral/Flutter-Generated.xcconfig',
             ],
-          ));
+            platformSupport: <String, PlatformDetails>{
+              platformMacOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        final Iterable<Directory> examplemacOSDirs = plugin.getExamples().map(
+            (RepositoryPackage example) =>
+                example.platformDirectory(FlutterPlatform.macos));
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--macos'],
+              null,
+            ),
+            for (final Directory directory in examplemacOSDirs)
+              ProcessCall(
+                'pod',
+                const <String>['install'],
+                directory.path,
+              ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('runs pub get if example is not configured', () async {
+        final RepositoryPackage plugin = createFakePlugin(
+            'plugin1', packagesDir, platformSupport: <String, PlatformDetails>{
+          platformMacOS: const PlatformDetails(PlatformSupport.inline)
+        });
+
+        final RepositoryPackage example = plugin.getExamples().first;
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos']);
+
+        expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            const ProcessCall(
+              'flutter',
+              <String>['precache', '--macos'],
+              null,
+            ),
+            ProcessCall(
+              'flutter',
+              const <String>['pub', 'get'],
+              example.directory.path,
+            ),
+            ProcessCall(
+              'pod',
+              const <String>['install'],
+              example.platformDirectory(FlutterPlatform.macos).path,
+            ),
+          ]),
+        );
+
+        expect(
+            output,
+            containsAllInOrder(<Matcher>[
+              contains('Running for plugin1'),
+              contains('No issues found!'),
+            ]));
+      });
+
+      test('fails if pre-pod pub get fails', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformMacOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        processRunner
+                .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
+            <FakeProcessInfo>[
+          FakeProcessInfo(MockProcess(), <String>['precache']),
+          FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get']),
+        ];
+
+        Error? commandError;
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos'],
+            errorHandler: (Error e) {
+          commandError = e;
+        });
+
+        expect(commandError, isA<ToolExit>());
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Unable to configure project'),
+              ],
+            ));
+      });
+
+      test('fails if pod install fails', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformMacOS: const PlatformDetails(PlatformSupport.inline)
+            });
+
+        processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
+          FakeProcessInfo(MockProcess(exitCode: 1)),
+        ];
+
+        Error? commandError;
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos'],
+            errorHandler: (Error e) {
+          commandError = e;
+        });
+
+        expect(commandError, isA<ToolExit>());
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('The following packages had errors:'),
+              ],
+            ));
+      });
+
+      test('skips non-macOS plugins', () async {
+        createFakePlugin('plugin1', packagesDir);
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native macOS dependencies.')
+              ],
+            ));
+      });
+
+      test('skips non-inline plugins', () async {
+        createFakePlugin('plugin1', packagesDir,
+            platformSupport: <String, PlatformDetails>{
+              platformMacOS: const PlatformDetails(PlatformSupport.federated)
+            });
+
+        final List<String> output = await runCapturingPrint(
+            runner, <String>['fetch-deps', '--no-dart', '--macos']);
+
+        expect(
+            output,
+            containsAllInOrder(
+              <Matcher>[
+                contains('Package does not have native macOS dependencies.')
+              ],
+            ));
+      });
     });
   });
 }
diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart
index 4f38dd4..1fd6abd 100644
--- a/script/tool/test/util.dart
+++ b/script/tool/test/util.dart
@@ -394,6 +394,7 @@
     String executable,
     List<String> args, {
     Directory? workingDir,
+    Map<String, String>? environment,
     bool exitOnError = false,
   }) async {
     recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
@@ -412,6 +413,7 @@
     String executable,
     List<String> args, {
     Directory? workingDir,
+    Map<String, String>? environment,
     bool exitOnError = false,
     bool logOnError = false,
     Encoding stdoutEncoding = io.systemEncoding,