Bundle ios dependencies (#34669)

This updates the flutter tool to cache binary files for ideviceinstaller, ios-deploy, libimobiledevice, and dynamically linked dependencies from Flutter's GCP bucket.
diff --git a/.cirrus.yml b/.cirrus.yml
index 204112c..6674fcc 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -21,7 +21,7 @@
     fingerprint_script: echo $OS; cat bin/internal/engine.version
   artifacts_cache:
     folder: bin/cache/artifacts
-    fingerprint_script: echo $OS; cat bin/internal/engine.version
+    fingerprint_script: echo $OS; cat bin/internal/*.version
   setup_script: ./dev/bots/cirrus_setup.sh
   matrix:
     - name: docs
@@ -298,7 +298,7 @@
     fingerprint_script: echo %OS% & type bin\internal\engine.version
   artifacts_cache:
     folder: bin\cache\artifacts
-    fingerprint_script: echo %OS% & type bin\internal\engine.version
+    fingerprint_script: echo %OS% & type bin\internal\*.version
   setup_script:
     - flutter config --no-analytics
     - flutter doctor -v
diff --git a/bin/internal/ideviceinstaller.version b/bin/internal/ideviceinstaller.version
new file mode 100644
index 0000000..90f1492
--- /dev/null
+++ b/bin/internal/ideviceinstaller.version
@@ -0,0 +1 @@
+ab9352110092cf651b5602301371cd00691c7e13
diff --git a/bin/internal/ios-deploy.version b/bin/internal/ios-deploy.version
new file mode 100644
index 0000000..7683618
--- /dev/null
+++ b/bin/internal/ios-deploy.version
@@ -0,0 +1 @@
+ea5583388ac0ca035f6b991fd7955bea6492c68c
diff --git a/bin/internal/libimobiledevice.version b/bin/internal/libimobiledevice.version
new file mode 100644
index 0000000..0fc801b
--- /dev/null
+++ b/bin/internal/libimobiledevice.version
@@ -0,0 +1 @@
+398c1208731cb887c64a31c2ae111048b079f80d
diff --git a/bin/internal/libplist.version b/bin/internal/libplist.version
new file mode 100644
index 0000000..973064b
--- /dev/null
+++ b/bin/internal/libplist.version
@@ -0,0 +1 @@
+17546f53ac1377b0d4f45a800aaec7366ba5b6a0
diff --git a/bin/internal/openssl.version b/bin/internal/openssl.version
new file mode 100644
index 0000000..400e36a
--- /dev/null
+++ b/bin/internal/openssl.version
@@ -0,0 +1 @@
+03da376ff7504c63a1d00d57cf41bd7b7e93ff65
diff --git a/bin/internal/usbmuxd.version b/bin/internal/usbmuxd.version
new file mode 100644
index 0000000..d77b5cf
--- /dev/null
+++ b/bin/internal/usbmuxd.version
@@ -0,0 +1 @@
+60109fdef47dfe0badfb558a6a2105e8fb23660a
diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart
index e6765c6..f6db339 100644
--- a/dev/devicelab/lib/framework/adb.dart
+++ b/dev/devicelab/lib/framework/adb.dart
@@ -392,9 +392,34 @@
     _workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
   }
 
+  // Returns the path to cached binaries relative to devicelab directory
+  String get _artifactDirPath {
+    return path.normalize(
+      path.join(
+        path.current,
+        '../../bin/cache/artifacts',
+      )
+    );
+  }
+
+  // Returns a colon-separated environment variable that contains the paths
+  // of linked libraries for idevice_id
+  Map<String, String> get _ideviceIdEnvironment {
+    final String libPath = const <String>[
+      'libimobiledevice',
+      'usbmuxd',
+      'libplist',
+      'openssl',
+      'ideviceinstaller',
+      'ios-deploy',
+    ].map((String packageName) => path.join(_artifactDirPath, packageName)).join(':');
+    return <String, String>{'DYLD_LIBRARY_PATH': libPath};
+  }
+
   @override
   Future<List<String>> discoverDevices() async {
-    final List<String> iosDeviceIDs = LineSplitter.split(await eval('idevice_id', <String>['-l']))
+    final String ideviceIdPath = path.join(_artifactDirPath, 'libimobiledevice', 'idevice_id');
+    final List<String> iosDeviceIDs = LineSplitter.split(await eval(ideviceIdPath, <String>['-l'], environment: _ideviceIdEnvironment))
       .map<String>((String line) => line.trim())
       .where((String line) => line.isNotEmpty)
       .toList();
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 65a8b63..191fd15 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -40,6 +40,14 @@
   kernelWorkerSnapshot,
   /// The root of the web implementation of the dart SDK.
   flutterWebSdk,
+  iosDeploy,
+  ideviceinfo,
+  ideviceId,
+  idevicename,
+  idevicesyslog,
+  idevicescreenshot,
+  ideviceinstaller,
+  iproxy,
   /// The root of the Linux desktop sources.
   linuxDesktopPath,
   /// The root of the Windows desktop sources.
@@ -93,6 +101,22 @@
       return 'dartdevc.dart.snapshot';
     case Artifact.kernelWorkerSnapshot:
       return 'kernel_worker.dart.snapshot';
+    case Artifact.iosDeploy:
+      return 'ios-deploy';
+    case Artifact.ideviceinfo:
+      return 'ideviceinfo';
+    case Artifact.ideviceId:
+      return 'idevice_id';
+    case Artifact.idevicename:
+      return 'idevicename';
+    case Artifact.idevicesyslog:
+      return 'idevicesyslog';
+    case Artifact.idevicescreenshot:
+      return 'idevicescreenshot';
+    case Artifact.ideviceinstaller:
+      return 'ideviceinstaller';
+    case Artifact.iproxy:
+      return 'iproxy';
     case Artifact.linuxDesktopPath:
       if (platform != TargetPlatform.linux_x64)  {
         throw Exception('${getNameForTargetPlatform(platform)} does not support'
@@ -187,13 +211,26 @@
   }
 
   String _getIosArtifactPath(Artifact artifact, TargetPlatform platform, BuildMode mode) {
-    final String engineDir = _getEngineArtifactsPath(platform, mode);
+    final String artifactFileName = _artifactToFileName(artifact);
     switch (artifact) {
       case Artifact.genSnapshot:
       case Artifact.snapshotDart:
       case Artifact.flutterFramework:
       case Artifact.frontendServerSnapshotForEngineDartSdk:
-        return fs.path.join(engineDir, _artifactToFileName(artifact));
+        final String engineDir = _getEngineArtifactsPath(platform, mode);
+        return fs.path.join(engineDir, artifactFileName);
+      case Artifact.ideviceId:
+      case Artifact.ideviceinfo:
+      case Artifact.idevicescreenshot:
+      case Artifact.idevicesyslog:
+      case Artifact.idevicename:
+        return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path;
+      case Artifact.iosDeploy:
+        return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path;
+      case Artifact.ideviceinstaller:
+        return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path;
+      case Artifact.iproxy:
+        return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path;
       default:
         assert(false, 'Artifact $artifact not available for platform $platform.');
         return null;
@@ -302,24 +339,25 @@
 
   @override
   String getArtifactPath(Artifact artifact, { TargetPlatform platform, BuildMode mode }) {
+    final String artifactFileName = _artifactToFileName(artifact);
     switch (artifact) {
       case Artifact.snapshotDart:
-        return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact));
+        return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', artifactFileName);
       case Artifact.genSnapshot:
         return _genSnapshotPath();
       case Artifact.flutterTester:
         return _flutterTesterPath(platform);
       case Artifact.isolateSnapshotData:
       case Artifact.vmSnapshotData:
-        return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact));
+        return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', artifactFileName);
       case Artifact.platformKernelDill:
-        return fs.path.join(_getFlutterPatchedSdkPath(mode), _artifactToFileName(artifact));
+        return fs.path.join(_getFlutterPatchedSdkPath(mode), artifactFileName);
       case Artifact.platformLibrariesJson:
-        return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', _artifactToFileName(artifact));
+        return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', artifactFileName);
       case Artifact.flutterFramework:
-        return fs.path.join(engineOutPath, _artifactToFileName(artifact));
+        return fs.path.join(engineOutPath, artifactFileName);
       case Artifact.flutterMacOSFramework:
-        return fs.path.join(engineOutPath, _artifactToFileName(artifact));
+        return fs.path.join(engineOutPath, artifactFileName);
       case Artifact.flutterPatchedSdkPath:
         // When using local engine always use [BuildMode.debug] regardless of
         // what was specified in [mode] argument because local engine will
@@ -329,23 +367,35 @@
       case Artifact.flutterWebSdk:
         return _getFlutterWebSdkPath();
       case Artifact.frontendServerSnapshotForEngineDartSdk:
-        return fs.path.join(_hostEngineOutPath, 'gen', _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, 'gen', artifactFileName);
       case Artifact.engineDartSdkPath:
         return fs.path.join(_hostEngineOutPath, 'dart-sdk');
       case Artifact.engineDartBinary:
-        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', artifactFileName);
       case Artifact.dart2jsSnapshot:
-        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName);
       case Artifact.dartdevcSnapshot:
-        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', artifactFileName);
       case Artifact.kernelWorkerSnapshot:
-        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName);
+      case Artifact.ideviceId:
+      case Artifact.ideviceinfo:
+      case Artifact.idevicename:
+      case Artifact.idevicescreenshot:
+      case Artifact.idevicesyslog:
+        return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path;
+      case Artifact.ideviceinstaller:
+        return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path;
+      case Artifact.iosDeploy:
+        return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path;
+      case Artifact.iproxy:
+        return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path;
       case Artifact.linuxDesktopPath:
-        return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, artifactFileName);
       case Artifact.windowsDesktopPath:
-        return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, artifactFileName);
       case Artifact.skyEnginePath:
-        return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact));
+        return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName);
     }
     assert(false, 'Invalid artifact $artifact.');
     return null;
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 47b2949..1dd5765 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -263,20 +263,26 @@
   return result;
 }
 
-bool exitsHappy(List<String> cli) {
+bool exitsHappy(
+  List<String> cli, {
+  Map<String, String> environment,
+}) {
   _traceCommand(cli);
   try {
-    return processManager.runSync(cli).exitCode == 0;
+    return processManager.runSync(cli, environment: environment).exitCode == 0;
   } catch (error) {
     printTrace('$cli failed with $error');
     return false;
   }
 }
 
-Future<bool> exitsHappyAsync(List<String> cli) async {
+Future<bool> exitsHappyAsync(
+  List<String> cli, {
+  Map<String, String> environment,
+}) async {
   _traceCommand(cli);
   try {
-    return (await processManager.run(cli)).exitCode == 0;
+    return (await processManager.run(cli, environment: environment)).exitCode == 0;
   } catch (error) {
     printTrace('$cli failed with $error');
     return false;
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 09af158..0f359b5 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -145,44 +145,6 @@
       'Once installed, run:\n'
       '  sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer';
 
-  // Messages used in IOSValidator
-  String get iOSIMobileDeviceMissing =>
-      'libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:\n'
-      '  brew update\n'
-      '  brew install --HEAD usbmuxd\n'
-      '  brew link usbmuxd\n'
-      '  brew install --HEAD libimobiledevice\n'
-      '  brew install ideviceinstaller';
-  String get iOSIMobileDeviceBroken =>
-      'Verify that all connected devices have been paired with this computer in Xcode.\n'
-      'If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.\n'
-      'To update with Brew, run:\n'
-      '  brew update\n'
-      '  brew uninstall --ignore-dependencies libimobiledevice\n'
-      '  brew uninstall --ignore-dependencies usbmuxd\n'
-      '  brew install --HEAD usbmuxd\n'
-      '  brew unlink usbmuxd\n'
-      '  brew link usbmuxd\n'
-      '  brew install --HEAD libimobiledevice\n'
-      '  brew install ideviceinstaller';
-  String get iOSDeviceInstallerMissing =>
-      'ideviceinstaller is not installed; this is used to discover connected iOS devices.\n'
-      'To install with Brew, run:\n'
-      '  brew install --HEAD usbmuxd\n'
-      '  brew link usbmuxd\n'
-      '  brew install --HEAD libimobiledevice\n'
-      '  brew install ideviceinstaller';
-  String iOSDeployVersion(String version) => 'ios-deploy $version';
-  String iOSDeployOutdated(String minVersion) =>
-      'ios-deploy out of date ($minVersion is required). To upgrade with Brew:\n'
-      '  brew upgrade ios-deploy';
-  String get iOSDeployMissing =>
-      'ios-deploy not installed. To install:\n'
-      '  brew install ios-deploy';
-  String get iOSBrewMissing =>
-      'Brew can be used to install tools for iOS device development.\n'
-      'Download brew at https://brew.sh/.';
-
   // Messages used in CocoaPodsValidator
   String cocoaPodsVersion(String version) => 'CocoaPods version $version';
   String cocoaPodsUninitialized(String consequence) =>
@@ -206,9 +168,6 @@
       '$consequence\n'
       'To upgrade:\n'
       '$upgradeInstructions';
-  String get cocoaPodsBrewMissing =>
-      'Brew can be used to install CocoaPods.\n'
-      'Download brew at https://brew.sh/.';
 
   // Messages used in VsCodeValidator
   String vsCodeVersion(String version) => 'version $version';
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index 6d2d13e..3a6039d 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -53,7 +53,7 @@
   /// Artifacts required by all developments.
   static const DevelopmentArtifact universal = DevelopmentArtifact._('universal');
 
-  /// The vaulues of DevelopmentArtifacts.
+  /// The values of DevelopmentArtifacts.
   static final List<DevelopmentArtifact> values = <DevelopmentArtifact>[
     android,
     iOS,
@@ -83,6 +83,9 @@
       _artifacts.add(LinuxEngineArtifacts(this));
       _artifacts.add(LinuxFuchsiaSDKArtifacts(this));
       _artifacts.add(MacOSFuchsiaSDKArtifacts(this));
+      for (String artifactName in IosUsbArtifacts.artifactNames) {
+        _artifacts.add(IosUsbArtifacts(artifactName, this));
+      }
     } else {
       _artifacts.addAll(artifacts);
     }
@@ -221,6 +224,22 @@
     return getCacheArtifacts().childDirectory(name);
   }
 
+  MapEntry<String, String> get dyLdLibEntry {
+    if (_dyLdLibEntry != null) {
+      return _dyLdLibEntry;
+    }
+    final List<String> paths = <String>[];
+    for (CachedArtifact artifact in _artifacts) {
+      final String currentPath = artifact.dyLdLibPath;
+      if (currentPath.isNotEmpty) {
+        paths.add(currentPath);
+      }
+    }
+    _dyLdLibEntry = MapEntry<String, String>('DYLD_LIBARY_PATH', paths.join(':'));
+    return _dyLdLibEntry;
+  }
+  MapEntry<String, String> _dyLdLibEntry;
+
   /// The web sdk has to be co-located with the dart-sdk so that they can share source
   /// code.
   Directory getWebSdkDirectory() {
@@ -328,6 +347,9 @@
   // artifact name.
   String get stampName => name;
 
+  /// Returns a string to be set as environment DYLD_LIBARY_PATH variable
+  String get dyLdLibPath => '';
+
   /// All development artifacts this cache provides.
   final Set<DevelopmentArtifact> developmentArtifacts;
 
@@ -890,6 +912,39 @@
   }
 }
 
+/// Cached iOS/USB binary artifacts.
+class IosUsbArtifacts extends CachedArtifact {
+  IosUsbArtifacts(String name, Cache cache) : super(
+    name,
+    cache,
+    // This is universal to ensure every command checks for them first
+    const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
+  );
+
+  static const List<String> artifactNames = <String>[
+    'libimobiledevice',
+    'usbmuxd',
+    'libplist',
+    'openssl',
+    'ideviceinstaller',
+    'ios-deploy',
+  ];
+
+  @override
+  String get dyLdLibPath {
+    return cache.getArtifactDirectory(name).path;
+  }
+
+  @override
+  Future<void> updateInner() {
+    if (!platform.isMacOS) {
+      return Future<void>.value();
+    }
+    final Uri archiveUri = Uri.parse('$_storageBaseUrl/flutter_infra/ios-usb-dependencies/$name/$version/$name.zip');
+    return _downloadZipArchive('Downloading $name...', archiveUri, location);
+  }
+}
+
 // Many characters are problematic in filenames, especially on Windows.
 final Map<int, List<int>> _flattenNameSubstitutions = <int, List<int>>{
   r'@'.codeUnitAt(0): '@@'.codeUnits,
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 533cfc6..ba5d910 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -87,9 +87,8 @@
       FuchsiaWorkflow: () => FuchsiaWorkflow(),
       GenSnapshot: () => const GenSnapshot(),
       HotRunnerConfig: () => HotRunnerConfig(),
-      IMobileDevice: () => const IMobileDevice(),
+      IMobileDevice: () => IMobileDevice(),
       IOSSimulatorUtils: () => IOSSimulatorUtils(),
-      IOSValidator: () => const IOSValidator(),
       IOSWorkflow: () => const IOSWorkflow(),
       KernelCompilerFactory: () => const KernelCompilerFactory(),
       LinuxWorkflow: () => const LinuxWorkflow(),
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 58ba4de..c4aa3a6 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -72,8 +72,6 @@
           GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]),
         if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
           GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]),
-        if (iosWorkflow.appliesToHostPlatform)
-          iosValidator,
         if (webWorkflow.appliesToHostPlatform)
           const WebValidator(),
         // Add desktop doctors to workflow if the flag is enabled.
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 3ac80c5..7007daf 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -7,6 +7,7 @@
 import 'package:meta/meta.dart';
 
 import '../application_package.dart';
+import '../artifacts.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
@@ -23,10 +24,6 @@
 import 'ios_workflow.dart';
 import 'mac.dart';
 
-const String _kIdeviceinstallerInstructions =
-    'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
-    'brew install ideviceinstaller.';
-
 class IOSDeploy {
   const IOSDeploy();
 
@@ -37,9 +34,15 @@
     @required String bundlePath,
     @required List<String> launchArguments,
   }) async {
-    final List<String> launchCommand = <String>[
+    final String iosDeployPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios);
+    // TODO(fujino): remove fallback once g3 updated
+    const List<String> fallbackIosDeployPath = <String>[
       '/usr/bin/env',
       'ios-deploy',
+    ];
+    final List<String> commandList = iosDeployPath != null ? <String>[iosDeployPath] : fallbackIosDeployPath;
+    final List<String> launchCommand = <String>[
+      ...commandList,
       '--id',
       deviceId,
       '--bundle',
@@ -61,6 +64,7 @@
     // it.
     final Map<String, String> iosDeployEnv = Map<String, String>.from(platform.environment);
     iosDeployEnv['PATH'] = '/usr/bin:${iosDeployEnv['PATH']}';
+    iosDeployEnv.addEntries(<MapEntry<String, String>>[cache.dyLdLibEntry]);
 
     return await runCommandAndStreamOutput(
       launchCommand,
@@ -120,8 +124,20 @@
           platformType: PlatformType.ios,
           ephemeral: true,
       ) {
-    _installerPath = _checkForCommand('ideviceinstaller');
-    _iproxyPath = _checkForCommand('iproxy');
+    if (platform.isMacOS) {
+      printError('Cannot control iOS devices or simulators. ideviceinstaller and iproxy are not available on your platform.');
+      _installerPath = null;
+      _iproxyPath = null;
+      return;
+    }
+    _installerPath = artifacts.getArtifactPath(
+      Artifact.ideviceinstaller,
+      platform: TargetPlatform.ios
+    ) ?? 'ideviceinstaller'; // TODO(fujino): remove fallback once g3 updated
+    _iproxyPath = artifacts.getArtifactPath(
+      Artifact.iproxy,
+      platform: TargetPlatform.ios
+    ) ?? 'iproxy'; // TODO(fujino): remove fallback once g3 updated
   }
 
   String _installerPath;
@@ -173,23 +189,6 @@
     return devices;
   }
 
-  static String _checkForCommand(
-    String command, [
-    String macInstructions = _kIdeviceinstallerInstructions,
-  ]) {
-    try {
-      command = runCheckedSync(<String>['which', command]).trim();
-    } catch (e) {
-      if (platform.isMacOS) {
-        printError('$command not found. $macInstructions');
-      } else {
-        printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
-      }
-      return null;
-    }
-    return command;
-  }
-
   @override
   Future<bool> isAppInstalled(ApplicationPackage app) async {
     try {
@@ -589,12 +588,17 @@
     while (!connected) {
       printTrace('attempting to forward device port $devicePort to host port $hostPort');
       // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
-      process = await runCommand(<String>[
-        device._iproxyPath,
-        hostPort.toString(),
-        devicePort.toString(),
-        device.id,
-      ]);
+      process = await runCommand(
+        <String>[
+          device._iproxyPath,
+          hostPort.toString(),
+          devicePort.toString(),
+          device.id,
+        ],
+        environment: Map<String, String>.fromEntries(
+          <MapEntry<String, String>>[cache.dyLdLibEntry],
+        ),
+      );
       // TODO(ianh): This is a flakey race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674
       connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false);
       if (!connected) {
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index cf95c30..f21dffd 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -2,21 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-
 import '../base/context.dart';
-import '../base/os.dart';
 import '../base/platform.dart';
-import '../base/process.dart';
-import '../base/user_messages.dart';
-import '../base/version.dart';
 import '../doctor.dart';
 import '../macos/xcode.dart';
-import 'mac.dart';
 import 'plist_utils.dart' as plist;
 
 IOSWorkflow get iosWorkflow => context.get<IOSWorkflow>();
-IOSValidator get iosValidator => context.get<IOSValidator>();
 
 class IOSWorkflow implements Workflow {
   const IOSWorkflow();
@@ -40,86 +32,3 @@
     return plist.getValueFromFile(path, key);
   }
 }
-
-class IOSValidator extends DoctorValidator {
-
-  const IOSValidator() : super('iOS tools - develop for iOS devices');
-
-  Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
-
-  Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);
-
-  String get iosDeployMinimumVersion => '1.9.4';
-
-  // ios-deploy <= v1.9.3 declares itself as v2.0.0
-  List<String> get iosDeployBadVersions => <String>['2.0.0'];
-
-  Future<String> get iosDeployVersionText async => (await runAsync(<String>['ios-deploy', '--version'])).processResult.stdout.replaceAll('\n', '');
-
-  bool get hasHomebrew => os.which('brew') != null;
-
-  Future<String> get macDevMode async => (await runAsync(<String>['DevToolsSecurity', '-status'])).processResult.stdout;
-
-  Future<bool> get _iosDeployIsInstalledAndMeetsVersionCheck async {
-    if (!await hasIosDeploy)
-      return false;
-    try {
-      final Version version = Version.parse(await iosDeployVersionText);
-      return version >= Version.parse(iosDeployMinimumVersion)
-          && !iosDeployBadVersions.map((String v) => Version.parse(v)).contains(version);
-    } on FormatException catch (_) {
-      return false;
-    }
-  }
-
-  // Change this value if the number of checks for packages needed for installation changes
-  static const int totalChecks = 4;
-
-  @override
-  Future<ValidationResult> validate() async {
-    final List<ValidationMessage> messages = <ValidationMessage>[];
-    ValidationType packageManagerStatus = ValidationType.installed;
-
-    int checksFailed = 0;
-
-    if (!iMobileDevice.isInstalled) {
-      checksFailed += 3;
-      packageManagerStatus = ValidationType.partial;
-      messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceMissing));
-    } else if (!await iMobileDevice.isWorking) {
-      checksFailed += 2;
-      packageManagerStatus = ValidationType.partial;
-      messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceBroken));
-    } else if (!await hasIDeviceInstaller) {
-      checksFailed += 1;
-      packageManagerStatus = ValidationType.partial;
-      messages.add(ValidationMessage.error(userMessages.iOSDeviceInstallerMissing));
-    }
-
-    final bool iHasIosDeploy = await hasIosDeploy;
-
-    // Check ios-deploy is installed at meets version requirements.
-    if (iHasIosDeploy) {
-      messages.add(ValidationMessage(userMessages.iOSDeployVersion(await iosDeployVersionText)));
-    }
-    if (!await _iosDeployIsInstalledAndMeetsVersionCheck) {
-      packageManagerStatus = ValidationType.partial;
-      if (iHasIosDeploy) {
-        messages.add(ValidationMessage.error(userMessages.iOSDeployOutdated(iosDeployMinimumVersion)));
-      } else {
-        checksFailed += 1;
-        messages.add(ValidationMessage.error(userMessages.iOSDeployMissing));
-      }
-    }
-
-    // If one of the checks for the packages failed, we may need brew so that we can install
-    // the necessary packages. If they're all there, however, we don't even need it.
-    if (checksFailed == totalChecks)
-      packageManagerStatus = ValidationType.missing;
-    if (checksFailed > 0 && !hasHomebrew) {
-      messages.add(ValidationMessage.hint(userMessages.iOSBrewMissing));
-    }
-
-    return ValidationResult(packageManagerStatus, messages);
-  }
-}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index dcc7f5a..2bcdd59 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -7,6 +7,7 @@
 import 'package:meta/meta.dart';
 
 import '../application_package.dart';
+import '../artifacts.dart';
 import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
@@ -42,35 +43,99 @@
 }
 
 class IMobileDevice {
-  const IMobileDevice();
+  IMobileDevice()
+      : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios)
+          ?? 'idevice_id', // TODO(fujino): remove fallback once g3 updated
+        _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios)
+          ?? 'ideviceinfo', // TODO(fujino): remove fallback once g3 updated
+        _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios)
+          ?? 'idevicename', // TODO(fujino): remove fallback once g3 updated
+        _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios)
+          ?? 'idevicesyslog', // TODO(fujino): remove fallback once g3 updated
+        _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios)
+          ?? 'idevicescreenshot' { // TODO(fujino): remove fallback once g3 updated
+        }
+  final String _ideviceIdPath;
+  final String _ideviceinfoPath;
+  final String _idevicenamePath;
+  final String _idevicesyslogPath;
+  final String _idevicescreenshotPath;
 
-  bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']);
+  bool get isInstalled {
+    _isInstalled ??= exitsHappy(
+      <String>[
+        _ideviceIdPath,
+        '-h'
+      ],
+      environment: Map<String, String>.fromEntries(
+        <MapEntry<String, String>>[cache.dyLdLibEntry]
+      ),
+    );
+    return _isInstalled;
+  }
+  bool _isInstalled;
 
   /// Returns true if libimobiledevice is installed and working as expected.
   ///
   /// Older releases of libimobiledevice fail to work with iOS 10.3 and above.
   Future<bool> get isWorking async {
-    if (!isInstalled)
-      return false;
+    if (_isWorking != null) {
+      return _isWorking;
+    }
+    if (!isInstalled) {
+      _isWorking = false;
+      return _isWorking;
+    }
     // If usage info is printed in a hyphenated id, we need to update.
     const String fakeIphoneId = '00008020-001C2D903C42002E';
-    final ProcessResult ideviceResult = (await runAsync(<String>['ideviceinfo', '-u', fakeIphoneId])).processResult;
+    final Map<String, String> executionEnv = Map<String, String>.fromEntries(
+      <MapEntry<String, String>>[cache.dyLdLibEntry]
+    );
+    final ProcessResult ideviceResult = (await runAsync(
+      <String>[
+        _ideviceinfoPath,
+        '-u',
+        fakeIphoneId
+      ],
+      environment: executionEnv,
+    )).processResult;
     if (ideviceResult.stdout.contains('Usage: ideviceinfo')) {
-      return false;
+      _isWorking = false;
+      return _isWorking;
     }
 
     // If no device is attached, we're unable to detect any problems. Assume all is well.
-    final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult;
-    if (result.exitCode == 0 && result.stdout.isEmpty)
-      return true;
-
-    // Check that we can look up the names of any attached devices.
-    return await exitsHappyAsync(<String>['idevicename']);
+    final ProcessResult result = (await runAsync(
+      <String>[
+        _ideviceIdPath,
+        '-l',
+      ],
+      environment: executionEnv,
+    )).processResult;
+    if (result.exitCode == 0 && result.stdout.isEmpty) {
+      _isWorking = true;
+    } else {
+      // Check that we can look up the names of any attached devices.
+      _isWorking = await exitsHappyAsync(
+        <String>[_idevicenamePath],
+        environment: executionEnv,
+      );
+    }
+    return _isWorking;
   }
+  bool _isWorking;
 
   Future<String> getAvailableDeviceIDs() async {
     try {
-      final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
+      final ProcessResult result = await processManager.run(
+        <String>[
+          _ideviceIdPath,
+          '-l'
+        ],
+        environment: Map<String, String>.fromEntries(
+          <MapEntry<String, String>>[cache.dyLdLibEntry]
+        ),
+      );
       if (result.exitCode != 0)
         throw ToolExit('idevice_id returned an error:\n${result.stderr}');
       return result.stdout;
@@ -81,7 +146,18 @@
 
   Future<String> getInfoForDevice(String deviceID, String key) async {
     try {
-      final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key]);
+      final ProcessResult result = await processManager.run(
+        <String>[
+          _ideviceinfoPath,
+          '-u',
+          deviceID,
+          '-k',
+          key
+        ],
+        environment: Map<String, String>.fromEntries(
+          <MapEntry<String, String>>[cache.dyLdLibEntry]
+        ),
+      );
       if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found'))
         throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}');
       if (result.exitCode != 0)
@@ -93,11 +169,30 @@
   }
 
   /// Starts `idevicesyslog` and returns the running process.
-  Future<Process> startLogger(String deviceID) => runCommand(<String>['idevicesyslog', '-u', deviceID]);
+  Future<Process> startLogger(String deviceID) {
+    return runCommand(
+      <String>[
+        _idevicesyslogPath,
+        '-u',
+        deviceID,
+      ],
+      environment: Map<String, String>.fromEntries(
+        <MapEntry<String, String>>[cache.dyLdLibEntry]
+      ),
+    );
+  }
 
   /// Captures a screenshot to the specified outputFile.
   Future<void> takeScreenshot(File outputFile) {
-    return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
+    return runCheckedAsync(
+      <String>[
+        _idevicescreenshotPath,
+        outputFile.path
+      ],
+      environment: Map<String, String>.fromEntries(
+        <MapEntry<String, String>>[cache.dyLdLibEntry]
+      ),
+    );
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
index 9462395..9729387 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart
@@ -30,11 +30,11 @@
   Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.''';
 
 const String cocoaPodsInstallInstructions = '''
-  brew install cocoapods
+  sudo gem install cocoapods
   pod setup''';
 
 const String cocoaPodsUpgradeInstructions = '''
-  brew upgrade cocoapods
+  sudo gem install cocoapods
   pod setup''';
 
 CocoaPods get cocoaPods => context.get<CocoaPods>();
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
index 615c3f5..aaee745 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 
 import '../base/context.dart';
-import '../base/os.dart';
 import '../base/user_messages.dart';
 import '../doctor.dart';
 import 'cocoapods.dart';
@@ -15,8 +14,6 @@
 class CocoaPodsValidator extends DoctorValidator {
   const CocoaPodsValidator() : super('CocoaPods subvalidator');
 
-  bool get hasHomebrew => os.which('brew') != null;
-
   @override
   Future<ValidationResult> validate() async {
     final List<ValidationMessage> messages = <ValidationMessage>[];
@@ -48,11 +45,6 @@
       }
     }
 
-    // Only check/report homebrew status if CocoaPods isn't installed.
-    if (status == ValidationType.missing && !hasHomebrew) {
-      messages.add(ValidationMessage.hint(userMessages.cocoaPodsBrewMissing));
-    }
-
     return ValidationResult(status, messages);
   }
-}
\ No newline at end of file
+}
diff --git a/packages/flutter_tools/test/cache_test.dart b/packages/flutter_tools/test/cache_test.dart
index a95964a..64a7e58 100644
--- a/packages/flutter_tools/test/cache_test.dart
+++ b/packages/flutter_tools/test/cache_test.dart
@@ -107,6 +107,22 @@
       verifyNever(artifact1.update(<DevelopmentArtifact>{}));
       verify(artifact2.update(<DevelopmentArtifact>{}));
     });
+    testUsingContext('getter dyLdLibEntry concatenates the output of each artifact\'s dyLdLibEntry getter', () async {
+      final CachedArtifact artifact1 = MockCachedArtifact();
+      final CachedArtifact artifact2 = MockCachedArtifact();
+      final CachedArtifact artifact3 = MockCachedArtifact();
+      when(artifact1.dyLdLibPath).thenReturn('/path/to/alpha:/path/to/beta');
+      when(artifact2.dyLdLibPath).thenReturn('/path/to/gamma:/path/to/delta:/path/to/epsilon');
+      when(artifact3.dyLdLibPath).thenReturn(''); // Empty output
+      final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2, artifact3]);
+      expect(cache.dyLdLibEntry.key, 'DYLD_LIBARY_PATH');
+      expect(
+        cache.dyLdLibEntry.value,
+        '/path/to/alpha:/path/to/beta:/path/to/gamma:/path/to/delta:/path/to/epsilon',
+      );
+    }, overrides: <Type, Generator>{
+      Cache: ()=> mockCache,
+    });
     testUsingContext('failed storage.googleapis.com download shows China warning', () async {
       final CachedArtifact artifact1 = MockCachedArtifact();
       final CachedArtifact artifact2 = MockCachedArtifact();
diff --git a/packages/flutter_tools/test/ios/code_signing_test.dart b/packages/flutter_tools/test/ios/code_signing_test.dart
index 9d381a0..ecea51f 100644
--- a/packages/flutter_tools/test/ios/code_signing_test.dart
+++ b/packages/flutter_tools/test/ios/code_signing_test.dart
@@ -425,7 +425,7 @@
       final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
 
       expect(
-        testLogger.errorText,
+        testLogger.errorText.replaceAll('\n', ' '),
         contains('Saved signing certificate "iPhone Developer: Invalid Profile" is not a valid development certificate'),
       );
       expect(
diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart
deleted file mode 100644
index a2e3745..0000000
--- a/packages/flutter_tools/test/ios/ios_workflow_test.dart
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:file/memory.dart';
-import 'package:flutter_tools/src/base/file_system.dart';
-import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/doctor.dart';
-import 'package:flutter_tools/src/ios/ios_workflow.dart';
-import 'package:flutter_tools/src/ios/mac.dart';
-import 'package:mockito/mockito.dart';
-import 'package:process/process.dart';
-
-import '../src/common.dart';
-import '../src/context.dart';
-
-void main() {
-  group('iOS Workflow validation', () {
-    MockIMobileDevice iMobileDevice;
-    MockIMobileDevice iMobileDeviceUninstalled;
-    MockProcessManager processManager;
-    FileSystem fs;
-
-    setUp(() {
-      iMobileDevice = MockIMobileDevice();
-      iMobileDeviceUninstalled = MockIMobileDevice(isInstalled: false);
-      processManager = MockProcessManager();
-      fs = MemoryFileSystem();
-    });
-
-    testUsingContext('Emit missing status when nothing is installed', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(
-        hasHomebrew: false,
-        hasIosDeploy: false,
-        hasIDeviceInstaller: false,
-        iosDeployVersionText: '0.0.0',
-      );
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.missing);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => iMobileDeviceUninstalled,
-    });
-
-    testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasHomebrew: false);
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.installed);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => iMobileDevice,
-    });
-
-    testUsingContext('Emits partial status when libimobiledevice is not installed', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => MockIMobileDevice(isInstalled: false, isWorking: false),
-    });
-
-    testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => MockIMobileDevice(isWorking: false),
-    });
-
-    testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
-      when(processManager.run(
-        <String>['ideviceinfo', '-u', '00008020-001C2D903C42002E'],
-        workingDirectory: anyNamed('workingDirectory'),
-        environment: anyNamed('environment')),
-      ).thenAnswer((Invocation _) async {
-        final MockProcessResult result = MockProcessResult();
-        when<String>(result.stdout).thenReturn(r'''
-Usage: ideviceinfo [OPTIONS]
-Show information about a connected device.
-
-  -d, --debug		enable communication debugging
-  -s, --simple		use a simple connection to avoid auto-pairing with the device
-  -u, --udid UDID	target specific device by its 40-digit device UDID
-  -q, --domain NAME	set domain of query to NAME. Default: None
-  -k, --key NAME	only query key specified by NAME. Default: All keys.
-  -x, --xml		output information as xml plist instead of key/value pairs
-  -h, --help		prints usage information
-        ''');
-        return null;
-      });
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => processManager,
-    });
-
-
-    testUsingContext('Emits partial status when ios-deploy is not installed', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasIosDeploy: false);
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => iMobileDevice,
-    });
-
-    testUsingContext('Emits partial status when ios-deploy version is too low', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0');
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => iMobileDevice,
-    });
-
-    testUsingContext('Emits partial status when ios-deploy version is a known bad version', () async {
-      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '2.0.0');
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.partial);
-    }, overrides: <Type, Generator>{
-      IMobileDevice: () => iMobileDevice,
-    });
-
-    testUsingContext('Succeeds when all checks pass', () async {
-      final ValidationResult result = await IOSWorkflowTestTarget().validate();
-      expect(result.type, ValidationType.installed);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => fs,
-      IMobileDevice: () => iMobileDevice,
-      ProcessManager: () => processManager,
-    });
-  });
-}
-
-final ProcessResult exitsHappy = ProcessResult(
-  1, // pid
-  0, // exitCode
-  '', // stdout
-  '', // stderr
-);
-
-class MockIMobileDevice extends IMobileDevice {
-  MockIMobileDevice({
-    this.isInstalled = true,
-    bool isWorking = true,
-  }) : isWorking = Future<bool>.value(isWorking);
-
-  @override
-  final bool isInstalled;
-
-  @override
-  final Future<bool> isWorking;
-}
-
-class MockProcessManager extends Mock implements ProcessManager {}
-class MockProcessResult extends Mock implements ProcessResult {}
-
-class IOSWorkflowTestTarget extends IOSValidator {
-  IOSWorkflowTestTarget({
-    this.hasHomebrew = true,
-    bool hasIosDeploy = true,
-    String iosDeployVersionText = '1.9.4',
-    bool hasIDeviceInstaller = true,
-  }) : hasIosDeploy = Future<bool>.value(hasIosDeploy),
-       iosDeployVersionText = Future<String>.value(iosDeployVersionText),
-       hasIDeviceInstaller = Future<bool>.value(hasIDeviceInstaller);
-
-  @override
-  final bool hasHomebrew;
-
-  @override
-  final Future<bool> hasIosDeploy;
-
-  @override
-  final Future<String> iosDeployVersionText;
-
-  @override
-  final Future<bool> hasIDeviceInstaller;
-}
diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart
index 8124ed8..de77d6e 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -9,6 +9,8 @@
 import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
 import 'package:flutter_tools/src/ios/mac.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/project.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
@@ -22,6 +24,8 @@
   Platform: _kNoColorTerminalPlatform,
 };
 
+class MockArtifacts extends Mock implements Artifacts {}
+class MockCache extends Mock implements Cache {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockFile extends Mock implements File {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
@@ -32,41 +36,80 @@
     final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform())
       ..operatingSystem = 'macos';
     MockProcessManager mockProcessManager;
+    final String libimobiledevicePath = fs.path.join('bin', 'cache', 'artifacts', 'libimobiledevice');
+    final String ideviceIdPath = fs.path.join(libimobiledevicePath, 'idevice_id');
+    final String ideviceInfoPath = fs.path.join(libimobiledevicePath, 'ideviceinfo');
+    final String idevicescreenshotPath = fs.path.join(libimobiledevicePath, 'idevicescreenshot');
+    MockArtifacts mockArtifacts;
+    MockCache mockCache;
 
     setUp(() {
       mockProcessManager = MockProcessManager();
+      mockCache = MockCache();
+      mockArtifacts = MockArtifacts();
+      when(mockArtifacts.getArtifactPath(Artifact.ideviceId, platform: anyNamed('platform'))).thenReturn(ideviceIdPath);
+      when(mockCache.dyLdLibEntry).thenReturn(
+        MapEntry<String, String>('DYLD_LIBRARY_PATH', libimobiledevicePath)
+      );
+    });
+
+    testUsingContext('isWorking returns false if libimobiledevice is not installed', () async {
+      when(mockProcessManager.runSync(
+        <String>[ideviceIdPath, '-h'], environment: anyNamed('environment')
+      )).thenReturn(ProcessResult(123, 1, '', ''));
+      expect(await iMobileDevice.isWorking, false);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Artifacts: () => mockArtifacts,
     });
 
     testUsingContext('getAvailableDeviceIDs throws ToolExit when libimobiledevice is not installed', () async {
-      when(mockProcessManager.run(<String>['idevice_id', '-l']))
-          .thenThrow(const ProcessException('idevice_id', <String>['-l']));
+      when(mockProcessManager.run(
+        <String>[ideviceIdPath, '-l'],
+        environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
+      )).thenThrow(ProcessException(ideviceIdPath, <String>['-l']));
       expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
+      Cache: () => mockCache,
+      Artifacts: () => mockArtifacts,
     });
 
     testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async {
-      when(mockProcessManager.run(<String>['idevice_id', '-l']))
-          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', 'Sad today')));
+      when(mockProcessManager.run(
+        <String>[ideviceIdPath, '-l'],
+        environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
+      )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', 'Sad today')));
       expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
+      Cache: () => mockCache,
+      Artifacts: () => mockArtifacts,
     });
 
     testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async {
-      when(mockProcessManager.run(<String>['idevice_id', '-l']))
-          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'foo', '')));
+      when(mockProcessManager.run(
+        <String>[ideviceIdPath, '-l'],
+        environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
+      )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'foo', '')));
       expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo');
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
+      Cache: () => mockCache,
+      Artifacts: () => mockArtifacts,
     });
 
     testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when ideviceinfo returns specific error code and message', () async {
-      when(mockProcessManager.run(<String>['ideviceinfo', '-u', 'foo', '-k', 'bar']))
-          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', '')));
+      when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath);
+      when(mockProcessManager.run(
+        <String>[ideviceInfoPath, '-u', 'foo', '-k', 'bar'],
+        environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
+      )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', '')));
       expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf<IOSDeviceNotFoundError>()));
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
+      Cache: () => mockCache,
+      Artifacts: () => mockArtifacts,
     });
 
     group('screenshot', () {
@@ -77,14 +120,15 @@
       setUp(() {
         mockProcessManager = MockProcessManager();
         mockOutputFile = MockFile();
+        when(mockArtifacts.getArtifactPath(Artifact.idevicescreenshot, platform: anyNamed('platform'))).thenReturn(idevicescreenshotPath);
       });
 
       testUsingContext('error if idevicescreenshot is not installed', () async {
         when(mockOutputFile.path).thenReturn(outputPath);
 
         // Let `idevicescreenshot` fail with exit code 1.
-        when(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
-            environment: null,
+        when(mockProcessManager.run(<String>[idevicescreenshotPath, outputPath],
+            environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
             workingDirectory: null,
         )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(4, 1, '', '')));
 
@@ -92,20 +136,23 @@
       }, overrides: <Type, Generator>{
         ProcessManager: () => mockProcessManager,
         Platform: () => osx,
+        Cache: () => mockCache,
       });
 
       testUsingContext('idevicescreenshot captures and returns screenshot', () async {
         when(mockOutputFile.path).thenReturn(outputPath);
-        when(mockProcessManager.run(any, environment: null, workingDirectory: null)).thenAnswer(
+        when(mockProcessManager.run(any, environment: anyNamed('environment'), workingDirectory: null)).thenAnswer(
             (Invocation invocation) => Future<ProcessResult>.value(ProcessResult(4, 0, '', '')));
 
         await iMobileDevice.takeScreenshot(mockOutputFile);
-        verify(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
-            environment: null,
+        verify(mockProcessManager.run(<String>[idevicescreenshotPath, outputPath],
+            environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
             workingDirectory: null,
         ));
       }, overrides: <Type, Generator>{
         ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+        Artifacts: () => mockArtifacts,
       });
     });
   });
diff --git a/packages/flutter_tools/test/macos/cocoapods_validator_test.dart b/packages/flutter_tools/test/macos/cocoapods_validator_test.dart
index bd49fc1..996bd35 100644
--- a/packages/flutter_tools/test/macos/cocoapods_validator_test.dart
+++ b/packages/flutter_tools/test/macos/cocoapods_validator_test.dart
@@ -23,7 +23,7 @@
     });
 
     testUsingContext('Emits installed status when CocoaPods is installed', () async {
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      const CocoaPodsValidator workflow = CocoaPodsValidator();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.installed);
     }, overrides: <Type, Generator>{
@@ -33,7 +33,7 @@
     testUsingContext('Emits missing status when CocoaPods is not installed', () async {
       when(cocoaPods.evaluateCocoaPodsInstallation)
           .thenAnswer((_) async => CocoaPodsStatus.notInstalled);
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      const CocoaPodsValidator workflow = CocoaPodsValidator();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.missing);
     }, overrides: <Type, Generator>{
@@ -43,7 +43,7 @@
     testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async {
       when(cocoaPods.evaluateCocoaPodsInstallation)
           .thenAnswer((_) async => CocoaPodsStatus.unknownVersion);
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      const CocoaPodsValidator workflow = CocoaPodsValidator();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
     }, overrides: <Type, Generator>{
@@ -52,7 +52,7 @@
 
     testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
       when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false);
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      const CocoaPodsValidator workflow = CocoaPodsValidator();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
     }, overrides: <Type, Generator>{
@@ -62,30 +62,13 @@
     testUsingContext('Emits partial status when CocoaPods version is too low', () async {
       when(cocoaPods.evaluateCocoaPodsInstallation)
           .thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion);
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      const CocoaPodsValidator workflow = CocoaPodsValidator();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
     }, overrides: <Type, Generator>{
       CocoaPods: () => cocoaPods,
     });
-
-    testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
-      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false);
-      final ValidationResult result = await workflow.validate();
-      expect(result.type, ValidationType.installed);
-    }, overrides: <Type, Generator>{
-      CocoaPods: () => cocoaPods,
-    });
   });
 }
 
 class MockCocoaPods extends Mock implements CocoaPods {}
-
-class CocoaPodsTestTarget extends CocoaPodsValidator {
-  CocoaPodsTestTarget({
-    this.hasHomebrew = true,
-  });
-
-  @override
-  final bool hasHomebrew;
-}