[flutter_releases] Flutter 1.22.4 framework cherrypicks (#70327)

* App.framework must support iOS 8 for older Flutter projects
* cherry pick fixes to https://github.com/flutter/flutter/issues/60118
* fix to --observatory-port flag
* update engine version

Co-authored-by: Jenn Magder <magder@google.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 45c0882..5aceb02 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-a1440ca392ca23e874a105c5f3248b495bd0e247
+2c956a31c0a3d350827aee6c56bb63337c5b4e6e
diff --git a/examples/flutter_view/ios/Flutter/AppFrameworkInfo.plist b/examples/flutter_view/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/flutter_view/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/flutter_view/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/hello_world/ios/Flutter/AppFrameworkInfo.plist b/examples/hello_world/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/hello_world/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/hello_world/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/image_list/ios/Flutter/AppFrameworkInfo.plist b/examples/image_list/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/image_list/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/image_list/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/layers/ios/Flutter/AppFrameworkInfo.plist b/examples/layers/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/layers/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/layers/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/platform_channel/ios/Flutter/AppFrameworkInfo.plist b/examples/platform_channel/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/platform_channel/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/platform_channel/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/platform_channel_swift/ios/Flutter/AppFrameworkInfo.plist b/examples/platform_channel_swift/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/platform_channel_swift/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/platform_channel_swift/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/examples/platform_view/ios/Flutter/AppFrameworkInfo.plist b/examples/platform_view/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/examples/platform_view/ios/Flutter/AppFrameworkInfo.plist
+++ b/examples/platform_view/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index f84d603..2c1a6ea 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -329,7 +329,7 @@
   }
 
   static Future<IOSApp> fromIosProject(IosProject project, BuildInfo buildInfo) {
-    if (getCurrentHostPlatform() != HostPlatform.darwin_x64) {
+    if (!globals.platform.isMacOS) {
       return null;
     }
     if (!project.exists) {
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index fadc637..fc097c2 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -241,7 +241,10 @@
     final List<String> commonBuildOptions = <String>[
       '-arch', targetArch,
       if (isIOS)
-        '-miphoneos-version-min=9.0',
+        // When the minimum version is updated, remember to update
+        // template MinimumOSVersion.
+        // https://github.com/flutter/flutter/pull/62902
+        '-miphoneos-version-min=8.0',
     ];
 
     const String embedBitcodeArg = '-fembed-bitcode';
diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart
index e57a8bc..54e7e9a 100644
--- a/packages/flutter_tools/lib/src/base/os.dart
+++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -7,6 +7,7 @@
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
 
+import '../build_info.dart';
 import '../globals.dart' as globals;
 import 'common.dart';
 import 'file_system.dart';
@@ -29,6 +30,13 @@
         platform: platform,
         processManager: processManager,
       );
+    } else if (platform.isMacOS) {
+      return _MacOSUtils(
+        fileSystem: fileSystem,
+        logger: logger,
+        platform: platform,
+        processManager: processManager,
+      );
     } else {
       return _PosixUtils(
         fileSystem: fileSystem,
@@ -114,6 +122,8 @@
     return osNames.containsKey(osName) ? osNames[osName] : osName;
   }
 
+  HostPlatform get hostPlatform;
+
   List<File> _which(String execName, { bool all = false });
 
   /// Returns the separator between items in the PATH environment variable.
@@ -243,29 +253,63 @@
     return _fileSystem.file(path);
   }
 
+  @override
+  String get pathVarSeparator => ':';
+
+  @override
+  HostPlatform hostPlatform = HostPlatform.linux_x64;
+}
+
+class _MacOSUtils extends _PosixUtils {
+  _MacOSUtils({
+    @required FileSystem fileSystem,
+    @required Logger logger,
+    @required Platform platform,
+    @required ProcessManager processManager,
+  }) : super(
+          fileSystem: fileSystem,
+          logger: logger,
+          platform: platform,
+          processManager: processManager,
+        );
+
   String _name;
 
   @override
   String get name {
     if (_name == null) {
-      if (_platform.isMacOS) {
-        final List<RunResult> results = <RunResult>[
-          _processUtils.runSync(<String>['sw_vers', '-productName']),
-          _processUtils.runSync(<String>['sw_vers', '-productVersion']),
-          _processUtils.runSync(<String>['sw_vers', '-buildVersion']),
-        ];
-        if (results.every((RunResult result) => result.exitCode == 0)) {
-          _name = '${results[0].stdout.trim()} ${results[1].stdout
-              .trim()} ${results[2].stdout.trim()}';
-        }
+      final List<RunResult> results = <RunResult>[
+        _processUtils.runSync(<String>['sw_vers', '-productName']),
+        _processUtils.runSync(<String>['sw_vers', '-productVersion']),
+        _processUtils.runSync(<String>['sw_vers', '-buildVersion']),
+      ];
+      if (results.every((RunResult result) => result.exitCode == 0)) {
+        _name =
+            '${results[0].stdout.trim()} ${results[1].stdout.trim()} ${results[2].stdout.trim()} ${getNameForHostPlatform(hostPlatform)}';
       }
       _name ??= super.name;
     }
     return _name;
   }
 
+  HostPlatform _hostPlatform;
+
+  // On ARM returns arm64, even when this process is running in Rosetta.
   @override
-  String get pathVarSeparator => ':';
+  HostPlatform get hostPlatform {
+    if (_hostPlatform == null) {
+      final RunResult arm64Check =
+          _processUtils.runSync(<String>['sysctl', 'hw.optional.arm64']);
+      // On arm64 stdout is "sysctl hw.optional.arm64: 1"
+      // On x86 hw.optional.arm64 is unavailable and exits with 1.
+      if (arm64Check.exitCode == 0 && arm64Check.stdout.trim().endsWith('1')) {
+        _hostPlatform = HostPlatform.darwin_arm;
+      } else {
+        _hostPlatform = HostPlatform.darwin_x64;
+      }
+    }
+    return _hostPlatform;
+  }
 }
 
 class _WindowsUtils extends OperatingSystemUtils {
@@ -282,6 +326,9 @@
   );
 
   @override
+  HostPlatform hostPlatform = HostPlatform.windows_x64;
+
+  @override
   void makeExecutable(File file) {}
 
   @override
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 04dadab..4fc8d2f 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -394,6 +394,7 @@
 
 enum HostPlatform {
   darwin_x64,
+  darwin_arm,
   linux_x64,
   windows_x64,
 }
@@ -402,6 +403,8 @@
   switch (platform) {
     case HostPlatform.darwin_x64:
       return 'darwin-x64';
+    case HostPlatform.darwin_arm:
+      return 'darwin-arm';
     case HostPlatform.linux_x64:
       return 'linux-x64';
     case HostPlatform.windows_x64:
@@ -414,6 +417,7 @@
 enum TargetPlatform {
   android,
   ios,
+  // darwin_arm64 not yet supported, macOS desktop targets run in Rosetta as x86.
   darwin_x64,
   linux_x64,
   windows_x64,
diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
index cd218a4..8948142 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart
@@ -236,14 +236,15 @@
       throw Exception('Failed to create App.framework.');
     }
     final List<String> lipoCommand = <String>[
-      'xcrun',
+      ...globals.xcode.xcrunCommand(),
       'lipo',
       '-create',
       iphoneFile.path,
       simulatorFile.path,
       '-output',
-      lipoOutputFile.path
+      lipoOutputFile.path,
     ];
+
     final RunResult lipoResult = await processUtils.run(
       lipoCommand,
     );
diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
index eba5be4..9d76323 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
@@ -318,7 +318,7 @@
 
         // Remove simulator architecture in profile and release mode.
         final List<String> lipoCommand = <String>[
-          'xcrun',
+          ...globals.xcode.xcrunCommand(),
           'lipo',
           fatFlutterFrameworkBinary.path,
           '-remove',
@@ -425,7 +425,7 @@
           'bitcode' : 'marker'; // In release, force bitcode embedding without archiving.
 
       List<String> pluginsBuildCommand = <String>[
-        'xcrun',
+        ...globals.xcode.xcrunCommand(),
         'xcodebuild',
         '-alltargets',
         '-sdk',
@@ -451,7 +451,7 @@
 
       if (mode == BuildMode.debug) {
         pluginsBuildCommand = <String>[
-          'xcrun',
+          ...globals.xcode.xcrunCommand(),
           'xcodebuild',
           '-alltargets',
           '-sdk',
@@ -503,7 +503,7 @@
               modeDirectory.childDirectory(podFrameworkName),
             );
             final List<String> lipoCommand = <String>[
-              'xcrun',
+              ...globals.xcode.xcrunCommand(),
               'lipo',
               '-create',
               globals.fs.path.join(podProduct.path, binaryName),
@@ -532,7 +532,7 @@
 
           if (boolArg('xcframework')) {
             final List<String> xcframeworkCommand = <String>[
-              'xcrun',
+              ...globals.xcode.xcrunCommand(),
               'xcodebuild',
               '-create-xcframework',
               '-framework',
@@ -616,7 +616,7 @@
 
       // Create iOS framework.
       List<String> lipoCommand = <String>[
-        'xcrun',
+        ...globals.xcode.xcrunCommand(),
         'lipo',
         fatFlutterFrameworkBinary.path,
         '-remove',
@@ -642,7 +642,7 @@
       globals.fsUtils.copyDirectorySync(fatFramework, simulatorFlutterFrameworkDirectory);
 
       lipoCommand = <String>[
-        'xcrun',
+        ...globals.xcode.xcrunCommand(),
         'lipo',
         fatFlutterFrameworkBinary.path,
         '-thin',
@@ -663,7 +663,7 @@
 
       // Create XCFramework from iOS and simulator frameworks.
       final List<String> xcframeworkCommand = <String>[
-        'xcrun',
+        ...globals.xcode.xcrunCommand(),
         'xcodebuild',
         '-create-xcframework',
         '-framework', armFlutterFrameworkDirectory.path,
@@ -696,7 +696,7 @@
     // Simulator is only supported in Debug mode.
     // "Fat" framework here must only contain arm.
     final List<String> xcframeworkCommand = <String>[
-      'xcrun',
+      ...globals.xcode.xcrunCommand(),
       'xcodebuild',
       '-create-xcframework',
       '-framework', fatFramework.path,
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index a245b85..823c146 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -193,10 +193,10 @@
   }
 
   final List<String> buildCommands = <String>[
-    '/usr/bin/env',
-    'xcrun',
+    ...globals.xcode.xcrunCommand(),
     'xcodebuild',
-    '-configuration', configuration,
+    '-configuration',
+    configuration,
   ];
 
   if (globals.logger.isVerbose) {
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index dcba2bb..fc7aa5a 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -25,7 +25,6 @@
 import 'mac.dart';
 import 'plist_parser.dart';
 
-const String _xcrunPath = '/usr/bin/xcrun';
 const String iosSimulatorId = 'apple_ios_simulator';
 
 class IOSSimulators extends PollingDeviceDiscovery {
@@ -51,8 +50,12 @@
     @required Xcode xcode,
     @required Logger logger,
     @required ProcessManager processManager,
-  }) : _simControl = SimControl(logger: logger, processManager: processManager),
-      _xcode = xcode;
+  })  : _simControl = SimControl(
+          logger: logger,
+          processManager: processManager,
+          xcode: xcode,
+        ),
+        _xcode = xcode;
 
   final SimControl _simControl;
   final Xcode _xcode;
@@ -80,11 +83,14 @@
   SimControl({
     @required Logger logger,
     @required ProcessManager processManager,
-  }) : _logger = logger,
-       _processUtils = ProcessUtils(processManager: processManager, logger: logger);
+    @required Xcode xcode,
+  })  : _logger = logger,
+        _xcode = xcode,
+        _processUtils = ProcessUtils(processManager: processManager, logger: logger);
 
   final Logger _logger;
   final ProcessUtils _processUtils;
+  final Xcode _xcode;
 
   /// Runs `simctl list --json` and returns the JSON of the corresponding
   /// [section].
@@ -106,7 +112,13 @@
     //   },
     //   "pairs": { ... },
 
-    final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'simctl',
+      'list',
+      '--json',
+      section.name,
+    ];
     _logger.printTrace(command.join(' '));
     final RunResult results = await _processUtils.run(command);
     if (results.exitCode != 0) {
@@ -155,7 +167,7 @@
 
   Future<bool> isInstalled(String deviceId, String appId) {
     return _processUtils.exitsHappy(<String>[
-      _xcrunPath,
+      ..._xcode.xcrunCommand(),
       'simctl',
       'get_app_container',
       deviceId,
@@ -167,7 +179,13 @@
     RunResult result;
     try {
       result = await _processUtils.run(
-        <String>[_xcrunPath, 'simctl', 'install', deviceId, appPath],
+        <String>[
+          ..._xcode.xcrunCommand(),
+          'simctl',
+          'install',
+          deviceId,
+          appPath,
+        ],
         throwOnError: true,
       );
     } on ProcessException catch (exception) {
@@ -180,7 +198,13 @@
     RunResult result;
     try {
       result = await _processUtils.run(
-        <String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId],
+        <String>[
+          ..._xcode.xcrunCommand(),
+          'simctl',
+          'uninstall',
+          deviceId,
+          appId,
+        ],
         throwOnError: true,
       );
     } on ProcessException catch (exception) {
@@ -194,7 +218,7 @@
     try {
       result = await _processUtils.run(
         <String>[
-          _xcrunPath,
+          ..._xcode.xcrunCommand(),
           'simctl',
           'launch',
           deviceId,
@@ -212,7 +236,14 @@
   Future<void> takeScreenshot(String deviceId, String outputPath) async {
     try {
       await _processUtils.run(
-        <String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath],
+        <String>[
+          ..._xcode.xcrunCommand(),
+          'simctl',
+          'io',
+          deviceId,
+          'screenshot',
+          outputPath,
+        ],
         throwOnError: true,
       );
     } on ProcessException catch (exception) {
@@ -316,8 +347,6 @@
   Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
   _IOSSimulatorDevicePortForwarder _portForwarder;
 
-  String get xcrunPath => globals.fs.path.join('/usr', 'bin', 'xcrun');
-
   @override
   Future<bool> isAppInstalled(
     ApplicationPackage app, {
@@ -424,7 +453,7 @@
         if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
         if (debuggingOptions.useTestFonts) '--use-test-fonts',
         if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"',
-        if (dartVmFlags.isNotEmpty) '--dart-flags=$dartVmFlags'
+        if (dartVmFlags.isNotEmpty) '--dart-flags=$dartVmFlags',
         '--observatory-port=${debuggingOptions.hostVmServicePort ?? 0}',
       ],
     ];
@@ -632,7 +661,16 @@
   ]);
 
   return processUtils.start(<String>[
-    _xcrunPath, 'simctl', 'spawn', device.id, 'log', 'stream', '--style', 'json', '--predicate', predicate,
+    ...globals.xcode.xcrunCommand(),
+    'simctl',
+    'spawn',
+    device.id,
+    'log',
+    'stream',
+    '--style',
+    'json',
+    '--predicate',
+    predicate,
   ]);
 }
 
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index fa824af..72bb6db 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -13,6 +13,7 @@
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
+import '../base/os.dart';
 import '../base/platform.dart';
 import '../base/process.dart';
 import '../build_info.dart';
@@ -63,13 +64,21 @@
     @required Logger logger,
     @required FileSystem fileSystem,
     @required XcodeProjectInterpreter xcodeProjectInterpreter,
-  }) : _platform = platform,
-       _fileSystem = fileSystem,
-       _xcodeProjectInterpreter = xcodeProjectInterpreter,
-       _processUtils = ProcessUtils(logger: logger, processManager: processManager);
+  })  : _platform = platform,
+        _fileSystem = fileSystem,
+        _xcodeProjectInterpreter = xcodeProjectInterpreter,
+        _operatingSystemUtils = OperatingSystemUtils(
+          fileSystem: fileSystem,
+          logger: logger,
+          platform: platform,
+          processManager: processManager,
+        ),
+        _processUtils =
+            ProcessUtils(logger: logger, processManager: processManager);
 
   final Platform _platform;
   final ProcessUtils _processUtils;
+  final OperatingSystemUtils _operatingSystemUtils;
   final FileSystem _fileSystem;
   final XcodeProjectInterpreter _xcodeProjectInterpreter;
 
@@ -110,7 +119,7 @@
     if (_eulaSigned == null) {
       try {
         final RunResult result = _processUtils.runSync(
-          <String>['/usr/bin/xcrun', 'clang'],
+          <String>[...xcrunCommand(), 'clang'],
         );
         if (result.stdout != null && result.stdout.contains('license')) {
           _eulaSigned = false;
@@ -135,7 +144,7 @@
         // This command will error if additional components need to be installed in
         // xcode 9.2 and above.
         final RunResult result = _processUtils.runSync(
-          <String>['/usr/bin/xcrun', 'simctl', 'list'],
+          <String>[...xcrunCommand(), 'simctl', 'list'],
         );
         _isSimctlInstalled = result.stderr == null || result.stderr == '';
       } on ProcessException {
@@ -161,16 +170,35 @@
     return false;
   }
 
+  /// The `xcrun` Xcode command to run or locate development
+  /// tools and properties.
+  ///
+  /// Returns `xcrun` on x86 macOS.
+  /// Returns `/usr/bin/arch -arm64e xcrun` on ARM macOS to force Xcode commands
+  /// to run outside the x86 Rosetta translation, which may cause crashes.
+  List<String> xcrunCommand() {
+    final List<String> xcrunCommand = <String>[];
+    if (_operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm) {
+      // Force Xcode commands to run outside Rosetta.
+      xcrunCommand.addAll(<String>[
+        '/usr/bin/arch',
+        '-arm64e',
+      ]);
+    }
+    xcrunCommand.add('xcrun');
+    return xcrunCommand;
+  }
+
   Future<RunResult> cc(List<String> args) {
     return _processUtils.run(
-      <String>['xcrun', 'cc', ...args],
+      <String>[...xcrunCommand(), 'cc', ...args],
       throwOnError: true,
     );
   }
 
   Future<RunResult> clang(List<String> args) {
     return _processUtils.run(
-      <String>['xcrun', 'clang', ...args],
+      <String>[...xcrunCommand(), 'clang', ...args],
       throwOnError: true,
     );
   }
@@ -178,7 +206,7 @@
   Future<String> sdkLocation(SdkType sdk) async {
     assert(sdk != null);
     final RunResult runResult = await _processUtils.run(
-      <String>['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'],
+      <String>[...xcrunCommand(), '--sdk', getNameForSdk(sdk), '--show-sdk-path'],
     );
     if (runResult.exitCode != 0) {
       throwToolExit('Could not find SDK location: ${runResult.stderr}');
@@ -260,28 +288,7 @@
     );
   }
 
-  bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null;
-
-  String _xcdevicePath;
-  String get xcdevicePath {
-    if (_xcdevicePath == null) {
-      try {
-        _xcdevicePath = _processUtils.runSync(
-          <String>[
-            'xcrun',
-            '--find',
-            'xcdevice'
-          ],
-          throwOnError: true,
-        ).stdout.trim();
-      } on ProcessException catch (exception) {
-        _logger.printTrace('Process exception finding xcdevice:\n$exception');
-      } on ArgumentError catch (exception) {
-        _logger.printTrace('Argument exception finding xcdevice:\n$exception');
-      }
-    }
-    return _xcdevicePath;
-  }
+  bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck;
 
   Future<List<dynamic>> _getAllDevices({
     bool useCache = false,
@@ -298,7 +305,7 @@
       // USB-tethered devices should be found quickly. 1 second timeout is faster than the default.
       final RunResult result = await _processUtils.run(
         <String>[
-          'xcrun',
+          ..._xcode.xcrunCommand(),
           'xcdevice',
           'list',
           '--timeout',
@@ -352,7 +359,7 @@
           '-t',
           '0',
           '/dev/null',
-          'xcrun',
+          ..._xcode.xcrunCommand(),
           'xcdevice',
           'observe',
           '--both',
diff --git a/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist b/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
+++ b/packages/flutter_tools/templates/app/ios.tmpl/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/AppFrameworkInfo.plist b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/AppFrameworkInfo.plist
index f2872cf..6b4c0f7 100644
--- a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/AppFrameworkInfo.plist
+++ b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>9.0</string>
+  <string>8.0</string>
 </dict>
 </plist>
diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart
index 8136345..1d7e13c 100644
--- a/packages/flutter_tools/test/general.shard/base/build_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/build_test.dart
@@ -17,6 +17,14 @@
 import '../../src/common.dart';
 import '../../src/context.dart';
 
+const FakeCommand kARMCheckCommand = FakeCommand(
+  command: <String>[
+    'sysctl',
+    'hw.optional.arm64',
+  ],
+  exitCode: 1,
+);
+
 const FakeCommand kSdkPathCommand = FakeCommand(
   command: <String>[
     'xcrun',
@@ -27,7 +35,7 @@
 );
 
 const List<String> kDefaultClang = <String>[
-  '-miphoneos-version-min=9.0',
+  '-miphoneos-version-min=8.0',
   '-dynamiclib',
   '-Xlinker',
   '-rpath',
@@ -47,7 +55,7 @@
 ];
 
 const List<String> kBitcodeClang = <String>[
-  '-miphoneos-version-min=9.0',
+  '-miphoneos-version-min=8.0',
   '-dynamiclib',
   '-Xlinker',
   '-rpath',
@@ -265,6 +273,7 @@
           'main.dill',
         ]
       ));
+      processManager.addCommand(kARMCheckCommand);
       processManager.addCommand(kSdkPathCommand);
       processManager.addCommand(const FakeCommand(
         command: <String>[
@@ -327,6 +336,7 @@
           'main.dill',
         ]
       ));
+      processManager.addCommand(kARMCheckCommand);
       processManager.addCommand(kSdkPathCommand);
       processManager.addCommand(const FakeCommand(
         command: <String>[
@@ -386,6 +396,7 @@
           'main.dill',
         ]
       ));
+      processManager.addCommand(kARMCheckCommand);
       processManager.addCommand(kSdkPathCommand);
       processManager.addCommand(const FakeCommand(
         command: <String>[
@@ -444,6 +455,7 @@
           'main.dill',
         ]
       ));
+      processManager.addCommand(kARMCheckCommand);
       processManager.addCommand(kSdkPathCommand);
       processManager.addCommand(const FakeCommand(
         command: <String>[
@@ -499,6 +511,7 @@
           'main.dill',
         ]
       ));
+      processManager.addCommand(kARMCheckCommand);
       processManager.addCommand(kSdkPathCommand);
       processManager.addCommand(const FakeCommand(
         command: <String>[
diff --git a/packages/flutter_tools/test/general.shard/base/os_test.dart b/packages/flutter_tools/test/general.shard/base/os_test.dart
index b697736..73e08ea 100644
--- a/packages/flutter_tools/test/general.shard/base/os_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/os_test.dart
@@ -6,10 +6,10 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
-import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
@@ -22,9 +22,11 @@
 
 void main() {
   MockProcessManager mockProcessManager;
+  FakeProcessManager fakeProcessManager;
 
   setUp(() {
     mockProcessManager = MockProcessManager();
+    fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
   });
 
   OperatingSystemUtils createOSUtils(Platform platform) {
@@ -32,28 +34,50 @@
       fileSystem: MemoryFileSystem(),
       logger: BufferLogger.test(),
       platform: platform,
-      processManager: mockProcessManager,
+      processManager: fakeProcessManager,
     );
   }
 
   group('which on POSIX', () {
     testWithoutContext('returns null when executable does not exist', () async {
-      when(mockProcessManager.runSync(<String>['which', kExecutable]))
-          .thenReturn(ProcessResult(0, 1, null, null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'which',
+            kExecutable,
+          ],
+          exitCode: 1,
+        ),
+      );
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux'));
       expect(utils.which(kExecutable), isNull);
     });
 
     testWithoutContext('returns exactly one result', () async {
-      when(mockProcessManager.runSync(<String>['which', 'foo']))
-          .thenReturn(ProcessResult(0, 0, kPath1, null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'which',
+            'foo',
+          ],
+          stdout: kPath1,
+        ),
+      );
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux'));
       expect(utils.which(kExecutable).path, kPath1);
     });
 
     testWithoutContext('returns all results for whichAll', () async {
-      when(mockProcessManager.runSync(<String>['which', '-a', kExecutable]))
-          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'which',
+            '-a',
+            kExecutable,
+          ],
+          stdout: '$kPath1\n$kPath2',
+        ),
+      );
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'linux'));
       final List<File> result = utils.whichAll(kExecutable);
       expect(result, hasLength(2));
@@ -66,27 +90,55 @@
     testWithoutContext('throws tool exit if where throws an argument error', () async {
       when(mockProcessManager.runSync(<String>['where', kExecutable]))
           .thenThrow(ArgumentError('Cannot find executable for where'));
-      final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows'));
+      final OperatingSystemUtils utils = OperatingSystemUtils(
+        fileSystem: MemoryFileSystem.test(),
+        logger: BufferLogger.test(),
+        platform: FakePlatform(operatingSystem: 'windows'),
+        processManager: mockProcessManager,
+      );
 
       expect(() => utils.which(kExecutable), throwsA(isA<ToolExit>()));
     });
+
     testWithoutContext('returns null when executable does not exist', () async {
-      when(mockProcessManager.runSync(<String>['where', kExecutable]))
-          .thenReturn(ProcessResult(0, 1, null, null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'where',
+            kExecutable,
+          ],
+          exitCode: 1,
+        ),
+      );
+
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows'));
       expect(utils.which(kExecutable), isNull);
     });
 
     testWithoutContext('returns exactly one result', () async {
-      when(mockProcessManager.runSync(<String>['where', 'foo']))
-          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'where',
+            'foo',
+          ],
+          stdout: '$kPath1\n$kPath2',
+        ),
+      );
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows'));
       expect(utils.which(kExecutable).path, kPath1);
     });
 
     testWithoutContext('returns all results for whichAll', () async {
-      when(mockProcessManager.runSync(<String>['where', kExecutable]))
-          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'where',
+            kExecutable,
+          ],
+          stdout: '$kPath1\n$kPath2',
+        ),
+      );
       final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows'));
       final List<File> result = utils.whichAll(kExecutable);
       expect(result, hasLength(2));
@@ -95,11 +147,162 @@
     });
   });
 
+  group('host platform', () {
+    testWithoutContext('unknown defaults to Linux', () async {
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'fuchsia'));
+      expect(utils.hostPlatform, HostPlatform.linux_x64);
+    });
+
+    testWithoutContext('Windows', () async {
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'windows'));
+      expect(utils.hostPlatform, HostPlatform.windows_x64);
+    });
+
+    testWithoutContext('Linux', () async {
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'linux'));
+      expect(utils.hostPlatform, HostPlatform.linux_x64);
+    });
+
+    testWithoutContext('macOS ARM', () async {
+      fakeProcessManager.addCommand(
+        const FakeCommand(
+          command: <String>[
+            'sysctl',
+            'hw.optional.arm64',
+          ],
+          stdout: 'hw.optional.arm64: 1',
+        ),
+      );
+
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'macos'));
+      expect(utils.hostPlatform, HostPlatform.darwin_arm);
+    });
+
+    testWithoutContext('macOS 11 x86', () async {
+      fakeProcessManager.addCommand(
+          const FakeCommand(
+            command: <String>[
+              'sysctl',
+              'hw.optional.arm64',
+            ],
+            stdout: 'hw.optional.arm64: 0',
+          ),
+          );
+
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'macos'));
+      expect(utils.hostPlatform, HostPlatform.darwin_x64);
+    });
+
+    testWithoutContext('macOS 10 x86', () async {
+      fakeProcessManager.addCommand(
+          const FakeCommand(
+            command: <String>[
+              'sysctl',
+              'hw.optional.arm64',
+            ],
+            exitCode: 1,
+          ),
+          );
+
+      final OperatingSystemUtils utils =
+      createOSUtils(FakePlatform(operatingSystem: 'macos'));
+      expect(utils.hostPlatform, HostPlatform.darwin_x64);
+    });
+
+    testWithoutContext('macOS ARM name', () async {
+      fakeProcessManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-productName',
+          ],
+          stdout: 'product',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-productVersion',
+          ],
+          stdout: 'version',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-buildVersion',
+          ],
+          stdout: 'build',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sysctl',
+            'hw.optional.arm64',
+          ],
+          stdout: 'hw.optional.arm64: 1',
+        ),
+      ]);
+
+      final OperatingSystemUtils utils =
+          createOSUtils(FakePlatform(operatingSystem: 'macos'));
+      expect(utils.name, 'product version build darwin-arm');
+    });
+
+    testWithoutContext('macOS x86 name', () async {
+      fakeProcessManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-productName',
+          ],
+          stdout: 'product',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-productVersion',
+          ],
+          stdout: 'version',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sw_vers',
+            '-buildVersion',
+          ],
+          stdout: 'build',
+        ),
+        const FakeCommand(
+          command: <String>[
+            'sysctl',
+            'hw.optional.arm64',
+          ],
+          exitCode: 1,
+        ),
+      ]);
+
+      final OperatingSystemUtils utils =
+          createOSUtils(FakePlatform(operatingSystem: 'macos'));
+      expect(utils.name, 'product version build darwin-x64');
+    });
+  });
+
   testWithoutContext('If unzip fails, include stderr in exception text', () {
     const String exceptionMessage = 'Something really bad happened.';
-    when(mockProcessManager.runSync(
-      <String>['unzip', '-o', '-q', null, '-d', null],
-    )).thenReturn(ProcessResult(0, 1, '', exceptionMessage));
+
+    fakeProcessManager.addCommand(
+      const FakeCommand(command: <String>[
+        'unzip',
+        '-o',
+        '-q',
+        null,
+        '-d',
+        null,
+      ], exitCode: 1, stderr: exceptionMessage),
+    );
+
     final MockFileSystem fileSystem = MockFileSystem();
     final MockFile mockFile = MockFile();
     final MockDirectory mockDirectory = MockDirectory();
@@ -111,7 +314,7 @@
       fileSystem: fileSystem,
       logger: BufferLogger.test(),
       platform: FakePlatform(operatingSystem: 'linux'),
-      processManager: mockProcessManager,
+      processManager: fakeProcessManager,
     );
 
     expect(
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
index 4c36f2f..4019f3d 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
@@ -457,6 +457,13 @@
         '--lazy-async-stacks',
         '$build/app.dill',
       ]),
+      const FakeCommand(
+        command: <String>[
+          'sysctl',
+          'hw.optional.arm64',
+        ],
+        exitCode: 1,
+      ),
       const FakeCommand(command: <String>[
         'xcrun',
         '--sdk',
@@ -498,7 +505,7 @@
         'clang',
         '-arch',
         'armv7',
-        '-miphoneos-version-min=9.0',
+        '-miphoneos-version-min=8.0',
         '-dynamiclib',
         '-Xlinker',
         '-rpath',
@@ -521,7 +528,7 @@
         'clang',
         '-arch',
         'arm64',
-        '-miphoneos-version-min=9.0',
+        '-miphoneos-version-min=8.0',
         '-dynamiclib',
         '-Xlinker',
         '-rpath',
@@ -575,6 +582,13 @@
         '--lazy-async-stacks',
         '$build/app.dill',
       ]),
+      const FakeCommand(
+        command: <String>[
+          'sysctl',
+          'hw.optional.arm64',
+        ],
+        exitCode: 1,
+      ),
       const FakeCommand(command: <String>[
         'xcrun',
         '--sdk',
@@ -600,7 +614,7 @@
         'clang',
         '-arch',
         'arm64',
-        '-miphoneos-version-min=9.0',
+        '-miphoneos-version-min=8.0',
         '-dynamiclib',
         '-Xlinker',
         '-rpath',
@@ -657,6 +671,13 @@
         '--lazy-async-stacks',
         '$build/app.dill',
       ]),
+      const FakeCommand(
+        command: <String>[
+          'sysctl',
+          'hw.optional.arm64',
+        ],
+        exitCode: 1,
+      ),
       const FakeCommand(command: <String>[
         'xcrun',
         '--sdk',
@@ -682,7 +703,7 @@
         'clang',
         '-arch',
         'arm64',
-        '-miphoneos-version-min=9.0',
+        '-miphoneos-version-min=8.0',
         '-dynamiclib',
         '-Xlinker',
         '-rpath',
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart
index 34fbcf4..347b939 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart
@@ -72,6 +72,13 @@
     environment.defines[kIosArchs] = 'arm64';
     processManager.addCommands(<FakeCommand>[
       // Create iphone stub.
+      const FakeCommand(
+        command: <String>[
+          'sysctl',
+          'hw.optional.arm64',
+        ],
+        exitCode: 1,
+      ),
       const FakeCommand(command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path']),
       FakeCommand(command: <String>[
         'xcrun',
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
index f46a180..7463934 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -37,7 +37,6 @@
 }
 
 const List<String> kRunReleaseArgs = <String>[
-  '/usr/bin/env',
   'xcrun',
   'xcodebuild',
   '-configuration',
@@ -94,6 +93,7 @@
       );
       mockXcode = MockXcode();
       when(mockXcode.isVersionSatisfactory).thenReturn(true);
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       fileSystem.file('foo/.packages')
         ..createSync(recursive: true)
         ..writeAsStringSync('\n');
diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
index 72bb8d9..1384825 100644
--- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
@@ -21,6 +21,7 @@
 import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
 import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/protocol_discovery.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
@@ -87,6 +88,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     }, testOn: 'posix');
   });
 
@@ -129,6 +131,7 @@
       FileSystemUtils: () => fsUtils,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     }, testOn: 'posix');
 
     testUsingContext('respects IOS_SIMULATOR_LOG_FILE_PATH', () {
@@ -145,6 +148,7 @@
       FileSystemUtils: () => fsUtils,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
   });
 
@@ -265,6 +269,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('Apple Watch is unsupported', () {
@@ -278,6 +283,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPad 2 is supported', () {
@@ -291,6 +297,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPad Retina is supported', () {
@@ -304,6 +311,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPhone 5 is supported', () {
@@ -317,6 +325,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPhone 5s is supported', () {
@@ -330,6 +339,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPhone SE is supported', () {
@@ -343,6 +353,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPhone 7 Plus is supported', () {
@@ -356,6 +367,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('iPhone X is supported', () {
@@ -369,6 +381,7 @@
       Platform: () => osx,
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
   });
 
@@ -391,7 +404,11 @@
         Future<ProcessResult>.value(ProcessResult(2, 0, '', ''))
       );
       // Test a real one. Screenshot doesn't require instance states.
-      final SimControl simControl = SimControl(processManager: mockProcessManager, logger: mockLogger);
+      final SimControl simControl = SimControl(
+        processManager: mockProcessManager,
+        logger: mockLogger,
+        xcode: mockXcode,
+      );
       // Doesn't matter what the device is.
       deviceUnderTest = IOSSimulator(
         'x',
@@ -399,6 +416,7 @@
         simControl: simControl,
         xcode: mockXcode,
       );
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
     });
 
     testWithoutContext(
@@ -421,7 +439,7 @@
         await deviceUnderTest.takeScreenshot(mockFile);
         verify(mockProcessManager.run(
           <String>[
-            '/usr/bin/xcrun',
+            'xcrun',
             'simctl',
             'io',
             'x',
@@ -446,6 +464,7 @@
         .thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
       mockSimControl = MockSimControl();
       mockXcode = MockXcode();
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
     });
 
     testUsingContext('syslog uses tail', () async {
@@ -469,7 +488,8 @@
       FileSystemUtils: () => FileSystemUtils(
         fileSystem: fileSystem,
         platform: macosPlatform,
-      )
+      ),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('unified logging with app name', () async {
@@ -491,7 +511,7 @@
 
       final List<String> command = verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single as List<String>;
       expect(command, <String>[
-        '/usr/bin/xcrun',
+        'xcrun',
         'simctl',
         'spawn',
         'x',
@@ -506,6 +526,7 @@
       overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
       FileSystem: () => fileSystem,
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('unified logging without app name', () async {
@@ -526,7 +547,7 @@
 
       final List<String> command = verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single as List<String>;
       expect(command, <String>[
-        '/usr/bin/xcrun',
+        'xcrun',
         'simctl',
         'spawn',
         'x',
@@ -541,6 +562,7 @@
       overrides: <Type, Generator>{
         ProcessManager: () => mockProcessManager,
         FileSystem: () => fileSystem,
+        Xcode: () => mockXcode,
       });
   });
 
@@ -555,6 +577,7 @@
       mockIosProject = MockIosProject();
       mockSimControl = MockSimControl();
       mockXcode = MockXcode();
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
     });
 
     group('syslog', () {
@@ -594,6 +617,7 @@
         ProcessManager: () => fakeProcessManager,
         FileSystem: () => fileSystem,
         Platform: () => osx,
+        Xcode: () => mockXcode,
       });
 
       testUsingContext('simulator can output `)`', () async {
@@ -630,6 +654,7 @@
         ProcessManager: () => fakeProcessManager,
         FileSystem: () => fileSystem,
         Platform: () => osx,
+        Xcode: () => mockXcode,
       });
 
       testUsingContext('multiline messages', () async {
@@ -682,6 +707,7 @@
         ProcessManager: () => fakeProcessManager,
         FileSystem: () => fileSystem,
         Platform: () => osx,
+        Xcode: () => mockXcode,
       });
     });
 
@@ -694,7 +720,7 @@
           'AND NOT(eventMessage CONTAINS " libxpc.dylib ")';
         fakeProcessManager.addCommand(const FakeCommand(
             command:  <String>[
-              '/usr/bin/xcrun',
+              'xcrun',
               'simctl',
               'spawn',
               '123456',
@@ -740,6 +766,7 @@
       }, overrides: <Type, Generator>{
         ProcessManager: () => fakeProcessManager,
         FileSystem: () => fileSystem,
+        Xcode: () => mockXcode,
       });
     });
   });
@@ -791,11 +818,13 @@
         return ProcessResult(mockPid, 0, validSimControlOutput, '');
       });
 
+      mockXcode = MockXcode();
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       simControl = SimControl(
         logger: mockLogger,
         processManager: mockProcessManager,
+        xcode: mockXcode,
       );
-      mockXcode = MockXcode();
     });
 
     testWithoutContext('getDevices succeeds', () async {
@@ -848,7 +877,7 @@
 
     testWithoutContext('.install() handles exceptions', () async {
       when(mockProcessManager.run(
-        <String>['/usr/bin/xcrun', 'simctl', 'install', deviceId, appId],
+        <String>['xcrun', 'simctl', 'install', deviceId, appId],
         environment: anyNamed('environment'),
         workingDirectory: anyNamed('workingDirectory'),
       )).thenThrow(const ProcessException('xcrun', <String>[]));
@@ -860,7 +889,7 @@
 
     testWithoutContext('.uninstall() handles exceptions', () async {
       when(mockProcessManager.run(
-        <String>['/usr/bin/xcrun', 'simctl', 'uninstall', deviceId, appId],
+        <String>['xcrun', 'simctl', 'uninstall', deviceId, appId],
         environment: anyNamed('environment'),
         workingDirectory: anyNamed('workingDirectory'),
       )).thenThrow(const ProcessException('xcrun', <String>[]));
@@ -872,7 +901,7 @@
 
     testWithoutContext('.launch() handles exceptions', () async {
       when(mockProcessManager.run(
-        <String>['/usr/bin/xcrun', 'simctl', 'launch', deviceId, appId],
+        <String>['xcrun', 'simctl', 'launch', deviceId, appId],
         environment: anyNamed('environment'),
         workingDirectory: anyNamed('workingDirectory'),
       )).thenThrow(const ProcessException('xcrun', <String>[]));
@@ -886,10 +915,13 @@
   group('startApp', () {
     SimControl simControl;
     MockXcode mockXcode;
+    MockPrototcolDiscovery mockPrototcolDiscovery;
 
     setUp(() {
       simControl = MockSimControl();
       mockXcode = MockXcode();
+      when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
+      mockPrototcolDiscovery = MockPrototcolDiscovery();
     });
 
     testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async {
@@ -901,19 +933,28 @@
         xcode: mockXcode,
       );
       when(globals.plistParser.getValueFromFile(any, any)).thenReturn('correct');
+      when(mockPrototcolDiscovery.uri)
+          .thenAnswer((_) async => Uri.parse('http://localhost:5678'));
 
       final Directory mockDir = globals.fs.currentDirectory;
       final IOSApp package = PrebuiltIOSApp(projectBundleId: 'incorrect', bundleName: 'name', bundleDir: mockDir);
 
       const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor', treeShakeIcons: false);
-      final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo);
+      final DebuggingOptions mockOptions =
+          DebuggingOptions.enabled(mockInfo, hostVmServicePort: 8888);
       await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);
 
-      verify(simControl.launch(any, 'correct', any));
+      verify(simControl.launch(any, 'correct', <String>[
+        '--enable-dart-profiling',
+        '--enable-checked-mode',
+        '--verify-entry-points',
+        '--observatory-port=8888',
+      ]));
     }, overrides: <Type, Generator>{
       PlistParser: () => MockPlistUtils(),
       FileSystem: () => fileSystem,
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
   });
 
@@ -947,6 +988,7 @@
     }, overrides: <Type, Generator>{
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
 
@@ -965,6 +1007,7 @@
     }, overrides: <Type, Generator>{
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
 
     testUsingContext('is false with no host app and no module', () async {
@@ -981,8 +1024,10 @@
     }, overrides: <Type, Generator>{
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
+      Xcode: () => mockXcode,
     });
   });
 }
 
+class MockPrototcolDiscovery extends Mock implements ProtocolDiscovery {}
 class MockBuildSystem extends Mock implements BuildSystem {}
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
index 0c5f6a2..ea24a99 100644
--- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -43,7 +43,7 @@
       setUp(() {
         xcode = Xcode(
           logger: logger,
-          platform: MockPlatform(),
+          platform: FakePlatform(operatingSystem: 'macos'),
           fileSystem: MemoryFileSystem.test(),
           processManager: processManager,
           xcodeProjectInterpreter: MockXcodeProjectInterpreter(),
@@ -61,8 +61,10 @@
       });
 
       testWithoutContext('eulaSigned is false when clang is not installed', () {
-        when(processManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
-          .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
+        when(processManager.runSync(<String>['sysctl', 'hw.optional.arm64']))
+            .thenReturn(ProcessResult(123, 1, '', ''));
+        when(processManager.runSync(<String>['xcrun', 'clang']))
+          .thenThrow(const ProcessException('xcrun', <String>['clang']));
 
         expect(xcode.eulaSigned, isFalse);
       });
@@ -83,23 +85,12 @@
           cache: MockCache(),
           iproxy: IProxy.test(logger: logger, processManager: processManager),
         );
-      });
-
-      testWithoutContext("xcrun can't find xcdevice", () {
-        when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-
-        when(processManager.runSync(<String>['xcrun', '--find', 'xcdevice']))
-          .thenThrow(const ProcessException('xcrun', <String>['--find', 'xcdevice']));
-        expect(xcdevice.isInstalled, false);
-        verify(processManager.runSync(any)).called(1);
+        when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       });
 
       testWithoutContext('available devices xcdevice fails', () async {
         when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
 
-        when(processManager.runSync(<String>['xcrun', '--find', 'xcdevice']))
-          .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', ''));
-
         when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2']))
           .thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']));
 
@@ -109,9 +100,6 @@
       testWithoutContext('diagnostics xcdevice fails', () async {
         when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
 
-        when(processManager.runSync(<String>['xcrun', '--find', 'xcdevice']))
-          .thenReturn(ProcessResult(1, 0, '/path/to/xcdevice', ''));
-
         when(processManager.run(<String>['xcrun', 'xcdevice', 'list', '--timeout', '2']))
           .thenThrow(const ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']));
 
@@ -128,210 +116,270 @@
     });
 
     group('Xcode', () {
-      Xcode xcode;
       MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
-      MockPlatform platform;
 
       setUp(() {
         mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
-        platform = MockPlatform();
-        xcode = Xcode(
+      });
+
+      testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
+        final Xcode xcode = Xcode(
           logger: logger,
-          platform: platform,
+          platform: FakePlatform(operatingSystem: 'windows'),
           fileSystem: MemoryFileSystem.test(),
           processManager: fakeProcessManager,
           xcodeProjectInterpreter: mockXcodeProjectInterpreter,
         );
-      });
-
-      testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () {
-        const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcode-select', '--print-path'],
-          stdout: xcodePath,
-        ));
-
-        expect(xcode.xcodeSelectPath, xcodePath);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isVersionSatisfactory, isFalse);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
-
-        expect(xcode.isVersionSatisfactory, isFalse);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isVersionSatisfactory, isTrue);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isVersionSatisfactory, isTrue);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isVersionSatisfactory, isTrue);
-      });
-
-      testWithoutContext('xcodeVersionSatisfactory is true when patch version exceeds minimum', () {
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
-
-        expect(xcode.isVersionSatisfactory, isTrue);
-      });
-
-      testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
-        when(platform.isMacOS).thenReturn(false);
 
         expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
       });
 
-      testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
-        when(platform.isMacOS).thenReturn(true);
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcode-select', '--print-path'],
-          stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-        ));
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+      group('macOS', () {
+        Xcode xcode;
+        FakePlatform platform;
 
-        expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
+        setUp(() {
+          mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+          platform = FakePlatform(operatingSystem: 'macos');
+          xcode = Xcode(
+            logger: logger,
+            platform: platform,
+            fileSystem: MemoryFileSystem.test(),
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: mockXcodeProjectInterpreter,
+          );
+        });
 
-      testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () {
-        when(platform.isMacOS).thenReturn(true);
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcode-select', '--print-path'],
-          exitCode: 127,
-          stderr: 'ERROR',
-        ));
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
-        when(platform.isMacOS).thenReturn(true);
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcode-select', '--print-path'],
-          stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-        ));
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
-        when(platform.isMacOS).thenReturn(true);
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcode-select', '--print-path'],
-          stdout: '/Applications/Xcode8.0.app/Contents/Developer',
-        ));
-        when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-        when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-        when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
-
-        expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcrun', 'clang'],
-          exitCode: 1,
-          stderr: 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.',
-        ));
-
-        expect(xcode.eulaSigned, isFalse);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
-        fakeProcessManager.addCommand(const FakeCommand(
-          command: <String>['/usr/bin/xcrun', 'clang'],
-          exitCode: 1,
-          stderr: 'clang: error: no input files',
-        ));
-
-        expect(xcode.eulaSigned, isTrue);
-        expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-      });
-
-      testWithoutContext('SDK name', () {
-        expect(getNameForSdk(SdkType.iPhone), 'iphoneos');
-        expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator');
-        expect(getNameForSdk(SdkType.macOS), 'macosx');
-      });
-
-      group('SDK location', () {
-        const String sdkroot = 'Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk';
-
-        testWithoutContext('--show-sdk-path iphoneos', () async {
+        testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () {
+          const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
           fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
-            stdout: sdkroot,
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            stdout: xcodePath,
           ));
 
-          expect(await xcode.sdkLocation(SdkType.iPhone), sdkroot);
+          expect(xcode.xcodeSelectPath, xcodePath);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
         });
 
-        testWithoutContext('--show-sdk-path macosx', () async {
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--sdk', 'macosx', '--show-sdk-path'],
-            stdout: sdkroot,
-          ));
+        testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
 
-          expect(await xcode.sdkLocation(SdkType.macOS), sdkroot);
+          expect(xcode.isVersionSatisfactory, isFalse);
+        });
+
+        testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+
+          expect(xcode.isVersionSatisfactory, isFalse);
+        });
+
+        testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+
+          expect(xcode.isVersionSatisfactory, isTrue);
+        });
+
+        testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+
+          expect(xcode.isVersionSatisfactory, isTrue);
+        });
+
+        testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+
+          expect(xcode.isVersionSatisfactory, isTrue);
+        });
+
+        testWithoutContext('xcodeVersionSatisfactory is true when patch version exceeds minimum', () {
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
+
+          expect(xcode.isVersionSatisfactory, isTrue);
+        });
+
+        testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
+          fakeProcessManager.addCommand(const FakeCommand(
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
+          ));
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+
+          expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
         });
 
-        testWithoutContext('--show-sdk-path fails', () async {
+        testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () {
           fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
-            exitCode: 1,
-            stderr: 'xcrun: error:',
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            exitCode: 127,
+            stderr: 'ERROR',
           ));
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
 
-          expect(() async => await xcode.sdkLocation(SdkType.iPhone),
-            throwsToolExit(message: 'Could not find SDK location'));
+          expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
         });
+
+        testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
+          fakeProcessManager.addCommand(const FakeCommand(
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
+          ));
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+
+          expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
+          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+        });
+
+        testWithoutContext('xcrun runs natively on arm64', () {
+          fakeProcessManager.addCommands(const <FakeCommand>[
+            FakeCommand(
+              command: <String>[
+                'sysctl',
+                'hw.optional.arm64',
+              ],
+              stdout: 'hw.optional.arm64: 1',
+            ),
+          ]);
+
+          expect(xcode.xcrunCommand(), <String>[
+            '/usr/bin/arch',
+            '-arm64e',
+            'xcrun',
+          ]);
+          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+        });
+
+        testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
+          fakeProcessManager.addCommand(const FakeCommand(
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            stdout: '/Applications/Xcode8.0.app/Contents/Developer',
+          ));
+          when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
+          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+
+          expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
+          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+        });
+
+        testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
+          fakeProcessManager.addCommands(const <FakeCommand>[
+            FakeCommand(
+              command: <String>[
+                'sysctl',
+                'hw.optional.arm64',
+              ],
+              exitCode: 1,
+            ),
+            FakeCommand(
+              command: <String>['xcrun', 'clang'],
+              exitCode: 1,
+              stderr:
+                  'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.',
+            ),
+          ]);
+
+          expect(xcode.eulaSigned, isFalse);
+          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+        });
+
+        testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
+          fakeProcessManager.addCommands(
+            const <FakeCommand>[
+              FakeCommand(
+                command: <String>[
+                  'sysctl',
+                  'hw.optional.arm64',
+                ],
+                exitCode: 1,
+              ),
+              FakeCommand(
+                command: <String>['xcrun', 'clang'],
+                exitCode: 1,
+                stderr: 'clang: error: no input files',
+              ),
+            ],
+          );
+          expect(xcode.eulaSigned, isTrue);
+          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+        });
+
+        testWithoutContext('SDK name', () {
+          expect(getNameForSdk(SdkType.iPhone), 'iphoneos');
+          expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator');
+          expect(getNameForSdk(SdkType.macOS), 'macosx');
+        });
+
+        group('SDK location', () {
+          setUp(() {
+            fakeProcessManager.addCommand(
+              const FakeCommand(
+                command: <String>[
+                  'sysctl',
+                  'hw.optional.arm64',
+                ],
+                exitCode: 1,
+              ),
+            );
+          });
+
+          const String sdkroot = 'Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk';
+
+          testWithoutContext('--show-sdk-path iphoneos', () async {
+            fakeProcessManager.addCommand(const FakeCommand(
+              command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
+              stdout: sdkroot,
+            ));
+
+            expect(await xcode.sdkLocation(SdkType.iPhone), sdkroot);
+            expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+          });
+
+          testWithoutContext('--show-sdk-path macosx', () async {
+            fakeProcessManager.addCommand(const FakeCommand(
+              command: <String>['xcrun', '--sdk', 'macosx', '--show-sdk-path'],
+              stdout: sdkroot,
+            ));
+
+            expect(await xcode.sdkLocation(SdkType.macOS), sdkroot);
+            expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+          });
+
+          testWithoutContext('--show-sdk-path fails', () async {
+            fakeProcessManager.addCommand(const FakeCommand(
+              command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
+              exitCode: 1,
+              stderr: 'xcrun: error:',
+            ));
+
+            expect(() async => await xcode.sdkLocation(SdkType.iPhone),
+              throwsToolExit(message: 'Could not find SDK location'));
+            expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+          });
+        });
       });
     });
 
@@ -354,6 +402,7 @@
           cache: mockCache,
           iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
         );
+        when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
       });
 
       group('installed', () {
@@ -361,17 +410,6 @@
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
           expect(xcdevice.isInstalled, false);
         });
-
-        testWithoutContext('is installed', () {
-          when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
-
-          expect(xcdevice.isInstalled, true);
-          expect(fakeProcessManager.hasRemainingExpectations, isFalse);
-        });
       });
 
       group('observe device events', () {
@@ -384,10 +422,6 @@
 
         testUsingContext('relays events', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           fakeProcessManager.addCommand(const FakeCommand(
             command: <String>[
@@ -446,10 +480,6 @@
 
         testUsingContext('returns devices', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           const String devicesOutput = '''
 [
@@ -570,10 +600,6 @@
 
         testWithoutContext('uses timeout', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           fakeProcessManager.addCommand(const FakeCommand(
             command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '20'],
@@ -585,10 +611,6 @@
 
         testUsingContext('ignores "Preparing debugger support for iPhone" error', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           const String devicesOutput = '''
 [
@@ -629,10 +651,6 @@
 
         testUsingContext('handles unknown architectures', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           const String devicesOutput = '''
 [
@@ -688,10 +706,6 @@
 
         testUsingContext('uses cache', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           const String devicesOutput = '''
 [
@@ -729,10 +743,6 @@
 
         testUsingContext('returns error message', () async {
           when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
-          fakeProcessManager.addCommand(const FakeCommand(
-            command: <String>['xcrun', '--find', 'xcdevice'],
-            stdout: '/path/to/xcdevice',
-          ));
 
           const String devicesOutput = '''
 [
@@ -842,6 +852,5 @@
 class MockXcode extends Mock implements Xcode {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
-class MockPlatform extends Mock implements Platform {}
 class MockArtifacts extends Mock implements Artifacts {}
 class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index f244796..8a164a1 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -18,6 +18,7 @@
 import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/build_runner/mustache_template.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/context_runner.dart';
 import 'package:flutter_tools/src/dart/pub.dart';
@@ -295,6 +296,9 @@
   ProcessResult makeExecutable(File file) => null;
 
   @override
+  HostPlatform hostPlatform = HostPlatform.linux_x64;
+
+  @override
   void chmod(FileSystemEntity entity, String mode) { }
 
   @override