Make Flutter tooling work on Android without Xcode being installed (#15161)

diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index ea4e9dc3..f8fd765 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -178,7 +178,7 @@
 
     final String plistPath = fs.path.join('ios', 'Runner', 'Info.plist');
     String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
-    if (id == null)
+    if (id == null || !xcodeProjectInterpreter.isInstalled)
       return null;
     final String projectPath = fs.path.join('ios', 'Runner.xcodeproj');
     final Map<String, String> buildSettings = xcodeProjectInterpreter.getBuildSettings(projectPath, 'Runner');
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart
index f9eebbb..e020c17 100644
--- a/packages/flutter_tools/lib/src/ios/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart
@@ -105,7 +105,7 @@
   /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
   /// include pods configuration.
   void setupPodfile(String appDirectory) {
-    if (!xcodeProjectInterpreter.canInterpretXcodeProjects) {
+    if (!xcodeProjectInterpreter.isInstalled) {
       // Don't do anything for iOS when host platform doesn't support it.
       return;
     }
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index d407bd2..55b5ed3 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -68,10 +68,10 @@
 
       messages.add(new ValidationMessage('Xcode at ${xcode.xcodeSelectPath}'));
 
-      xcodeVersionInfo = xcode.xcodeVersionText;
+      xcodeVersionInfo = xcode.versionText;
       if (xcodeVersionInfo.contains(','))
         xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
-      messages.add(new ValidationMessage(xcode.xcodeVersionText));
+      messages.add(new ValidationMessage(xcode.versionText));
 
       if (!xcode.isInstalledAndMeetsVersionCheck) {
         xcodeStatus = ValidationType.partial;
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 0d48146..49eb677 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -104,7 +104,7 @@
 }
 
 class Xcode {
-  bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory;
+  bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
 
   String _xcodeSelectPath;
   String get xcodeSelectPath {
@@ -121,11 +121,15 @@
   bool get isInstalled {
     if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
       return false;
-    if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText))
-      return false;
-    return true;
+    return xcodeProjectInterpreter.isInstalled;
   }
 
+  int get majorVersion => xcodeProjectInterpreter.majorVersion;
+
+  int get minorVersion => xcodeProjectInterpreter.minorVersion;
+
+  String get versionText => xcodeProjectInterpreter.versionText;
+
   bool _eulaSigned;
   /// Has the EULA been signed?
   bool get eulaSigned {
@@ -145,61 +149,17 @@
     return _eulaSigned;
   }
 
-  final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)');
-  void _updateXcodeVersion() {
-    try {
-      _xcodeVersionText = processManager.runSync(<String>['/usr/bin/xcodebuild', '-version']).stdout.trim().replaceAll('\n', ', ');
-      final Match match = xcodeVersionRegex.firstMatch(xcodeVersionText);
-      if (match == null)
-        return;
-
-      final String version = match.group(1);
-      final List<String> components = version.split('.');
-      _xcodeMajorVersion = int.parse(components[0]);
-      _xcodeMinorVersion = components.length == 1 ? 0 : int.parse(components[1]);
-    } on ProcessException {
-      // Ignore: leave values null.
-    }
-  }
-
-  String _xcodeVersionText;
-  String get xcodeVersionText {
-    if (_xcodeVersionText == null)
-      _updateXcodeVersion();
-    return _xcodeVersionText;
-  }
-
-  int _xcodeMajorVersion;
-  int get xcodeMajorVersion {
-    if (_xcodeMajorVersion == null)
-      _updateXcodeVersion();
-    return _xcodeMajorVersion;
-  }
-
-  int _xcodeMinorVersion;
-  int get xcodeMinorVersion {
-    if (_xcodeMinorVersion == null)
-      _updateXcodeVersion();
-    return _xcodeMinorVersion;
-  }
-
-  bool get xcodeVersionSatisfactory {
-    if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText))
+  bool get isVersionSatisfactory {
+    if (!xcodeProjectInterpreter.isInstalled)
       return false;
-    return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion);
+    if (majorVersion > kXcodeRequiredVersionMajor)
+      return true;
+    if (majorVersion == kXcodeRequiredVersionMajor)
+      return minorVersion >= kXcodeRequiredVersionMinor;
+    return false;
   }
 }
 
-bool _xcodeVersionCheckValid(int major, int minor) {
-  if (major > kXcodeRequiredVersionMajor)
-    return true;
-
-  if (major == kXcodeRequiredVersionMajor)
-    return minor >= kXcodeRequiredVersionMinor;
-
-  return false;
-}
-
 Future<XcodeBuildResult> buildXcodeProject({
   BuildableIOSApp app,
   BuildInfo buildInfo,
@@ -547,23 +507,19 @@
   final Map<String, String> buildSettings;
 }
 
-final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
 final String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
 
 bool _checkXcodeVersion() {
   if (!platform.isMacOS)
     return false;
-  try {
-    final String version = runCheckedSync(<String>['xcodebuild', '-version']);
-    final Match match = _xcodeVersionRegExp.firstMatch(version);
-    if (int.parse(match[1]) < kXcodeRequiredVersionMajor) {
-      printError('Found "${match[0]}". $_xcodeRequirement');
-      return false;
-    }
-  } catch (e) {
+  if (!xcodeProjectInterpreter.isInstalled) {
     printError('Cannot find "xcodebuild". $_xcodeRequirement');
     return false;
   }
+  if (!xcode.isVersionSatisfactory) {
+    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
+    return false;
+  }
   return true;
 }
 
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index f9d5b22..c2222df 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -477,7 +477,7 @@
   }
 
   bool get _xcodeVersionSupportsScreenshot {
-    return xcode.xcodeMajorVersion > 8 || (xcode.xcodeMajorVersion == 8 && xcode.xcodeMinorVersion >= 2);
+    return xcode.majorVersion > 8 || (xcode.majorVersion == 8 && xcode.minorVersion >= 2);
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 9a50d30..e35d6aa 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -7,7 +7,10 @@
 import '../artifacts.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/platform.dart';
 import '../base/process.dart';
+import '../base/process_manager.dart';
 import '../base/utils.dart';
 import '../build_info.dart';
 import '../cache.dart';
@@ -84,16 +87,58 @@
 
 XcodeProjectInterpreter get xcodeProjectInterpreter => context.putIfAbsent(
   XcodeProjectInterpreter,
-  () => const XcodeProjectInterpreter(),
+  () => new XcodeProjectInterpreter(),
 );
 
-/// Interpreter of Xcode projects settings.
+/// Interpreter of Xcode projects.
 class XcodeProjectInterpreter {
   static const String _executable = '/usr/bin/xcodebuild';
+  static final RegExp _versionRegex = new RegExp(r'Xcode ([0-9.]+)');
 
-  const XcodeProjectInterpreter();
+  void _updateVersion() {
+    if (!platform.isMacOS || !fs.file(_executable).existsSync()) {
+      return;
+    }
+    try {
+      final ProcessResult result = processManager.runSync(<String>[_executable, '-version']);
+      if (result.exitCode != 0) {
+        return;
+      }
+      _versionText = result.stdout.trim().replaceAll('\n', ', ');
+      final Match match = _versionRegex.firstMatch(versionText);
+      if (match == null)
+        return;
+      final String version = match.group(1);
+      final List<String> components = version.split('.');
+      _majorVersion = int.parse(components[0]);
+      _minorVersion = components.length == 1 ? 0 : int.parse(components[1]);
+    } on ProcessException {
+      // Ignore: leave values null.
+    }
+  }
 
-  bool get canInterpretXcodeProjects => fs.isFileSync(_executable);
+  bool get isInstalled => majorVersion != null;
+
+  String _versionText;
+  String get versionText {
+    if (_versionText == null)
+      _updateVersion();
+    return _versionText;
+  }
+
+  int _majorVersion;
+  int get majorVersion {
+    if (_majorVersion == null)
+      _updateVersion();
+    return _majorVersion;
+  }
+
+  int _minorVersion;
+  int get minorVersion {
+    if (_minorVersion == null)
+      _updateVersion();
+    return _minorVersion;
+  }
 
   Map<String, String> getBuildSettings(String projectPath, String target) {
     final String out = runCheckedSync(<String>[
diff --git a/packages/flutter_tools/test/ios/cocoapods_test.dart b/packages/flutter_tools/test/ios/cocoapods_test.dart
index 4eccc62..825bbe4 100644
--- a/packages/flutter_tools/test/ios/cocoapods_test.dart
+++ b/packages/flutter_tools/test/ios/cocoapods_test.dart
@@ -70,7 +70,7 @@
     });
 
     testUsingContext('creates swift Podfile if swift', () {
-      when(mockXcodeProjectInterpreter.canInterpretXcodeProjects).thenReturn(true);
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
       when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
         'SWIFT_VERSION': '4.0',
       });
@@ -94,7 +94,7 @@
     });
 
     testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () {
-      when(mockXcodeProjectInterpreter.canInterpretXcodeProjects).thenReturn(false);
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
 
       cocoaPodsUnderTest.setupPodfile('project');
 
diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart
index 1b222f3..401947a 100644
--- a/packages/flutter_tools/test/ios/ios_workflow_test.dart
+++ b/packages/flutter_tools/test/ios/ios_workflow_test.dart
@@ -79,7 +79,7 @@
 
     testUsingContext('Emits partial status when Xcode version too low', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
       when(xcode.eulaSigned).thenReturn(true);
@@ -94,7 +94,7 @@
 
     testUsingContext('Emits partial status when Xcode EULA not signed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(false);
@@ -109,7 +109,7 @@
 
     testUsingContext('Emits partial status when Mac dev mode was never enabled', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -124,7 +124,7 @@
 
     testUsingContext('Emits partial status when python six not installed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -139,7 +139,7 @@
 
     testUsingContext('Emits partial status when homebrew not installed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -154,7 +154,7 @@
 
     testUsingContext('Emits partial status when libimobiledevice is not installed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -169,7 +169,7 @@
 
     testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -184,7 +184,7 @@
 
     testUsingContext('Emits partial status when ios-deploy is not installed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -199,7 +199,7 @@
 
     testUsingContext('Emits partial status when ios-deploy version is too low', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -214,7 +214,7 @@
 
     testUsingContext('Emits partial status when CocoaPods is not installed', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -231,7 +231,7 @@
 
     testUsingContext('Emits partial status when CocoaPods version is too low', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -249,7 +249,7 @@
 
     testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
@@ -269,7 +269,7 @@
 
     testUsingContext('Succeeds when all checks pass', () async {
       when(xcode.isInstalled).thenReturn(true);
-      when(xcode.xcodeVersionText)
+      when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart
index 7110f4e..fc7b5a5 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -8,6 +8,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 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:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 import 'package:process/process.dart';
@@ -18,6 +19,7 @@
 
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockFile extends Mock implements File {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
 
 void main() {
   group('IMobileDevice', () {
@@ -97,9 +99,11 @@
   group('Xcode', () {
     MockProcessManager mockProcessManager;
     Xcode xcode;
+    MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
 
     setUp(() {
       mockProcessManager = new MockProcessManager();
+      mockXcodeProjectInterpreter = new MockXcodeProjectInterpreter();
       xcode = new Xcode();
     });
 
@@ -120,100 +124,47 @@
       ProcessManager: () => mockProcessManager,
     });
 
-    testUsingContext('xcodeVersionText returns null when xcodebuild is not installed', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenThrow(const ProcessException('/usr/bin/xcodebuild', const <String>['-version']));
-      expect(xcode.xcodeVersionText, isNull);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeVersionText returns formatted version text', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeVersionText, 'Xcode 8.3.3, Build version 8E3004b');
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeVersionText handles Xcode version string with unexpected format', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeVersionText, 'Xcode Ultra5000, Build version 8E3004b');
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeMajorVersion returns major version', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeMajorVersion, 8);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeMajorVersion is null when version has unexpected format', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeMajorVersion, isNull);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeMinorVersion returns minor version', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeMinorVersion, 3);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeMinorVersion returns 0 when minor version is unspecified', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeMinorVersion, 0);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('xcodeMinorVersion is null when version has unexpected format', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeMinorVersion, isNull);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
-
     testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
-      expect(xcode.xcodeVersionSatisfactory, isFalse);
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(8);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(17);
+      expect(xcode.isVersionSatisfactory, isFalse);
     }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
     });
 
-    testUsingContext('xcodeVersionSatisfactory is false when version in unknown format', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode SuperHD\nBuild version 7A1001', ''));
-      expect(xcode.xcodeVersionSatisfactory, isFalse);
+    testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+      expect(xcode.isVersionSatisfactory, isFalse);
     }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
     });
 
     testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 9.0\nBuild version 9A235', ''));
-      expect(xcode.xcodeVersionSatisfactory, isTrue);
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+      expect(xcode.isVersionSatisfactory, isTrue);
     }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
     });
 
-    testUsingContext('xcodeVersionSatisfactory is true when version exceeds minimum', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcodebuild', '-version']))
-          .thenReturn(new ProcessResult(1, 0, 'Xcode 10.0\nBuild version 10A123', ''));
-      expect(xcode.xcodeVersionSatisfactory, isTrue);
+    testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+      expect(xcode.isVersionSatisfactory, isTrue);
     }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
+      expect(xcode.isVersionSatisfactory, isTrue);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
     });
 
     testUsingContext('eulaSigned is false when clang is not installed', () {
diff --git a/packages/flutter_tools/test/ios/simulators_test.dart b/packages/flutter_tools/test/ios/simulators_test.dart
index 53deb2e..9ef1cf4 100644
--- a/packages/flutter_tools/test/ios/simulators_test.dart
+++ b/packages/flutter_tools/test/ios/simulators_test.dart
@@ -165,8 +165,8 @@
     testUsingContext(
       'old Xcode doesn\'t support screenshot',
       () {
-        when(mockXcode.xcodeMajorVersion).thenReturn(7);
-        when(mockXcode.xcodeMinorVersion).thenReturn(1);
+        when(mockXcode.majorVersion).thenReturn(7);
+        when(mockXcode.minorVersion).thenReturn(1);
         expect(deviceUnderTest.supportsScreenshot, false);
       },
       overrides: <Type, Generator>{Xcode: () => mockXcode}
@@ -175,8 +175,8 @@
     testUsingContext(
       'Xcode 8.2+ supports screenshots',
       () async {
-        when(mockXcode.xcodeMajorVersion).thenReturn(8);
-        when(mockXcode.xcodeMinorVersion).thenReturn(2);
+        when(mockXcode.majorVersion).thenReturn(8);
+        when(mockXcode.minorVersion).thenReturn(2);
         expect(deviceUnderTest.supportsScreenshot, true);
         final MockFile mockFile = new MockFile();
         when(mockFile.path).thenReturn(fs.path.join('some', 'path', 'to', 'screenshot.png'));
diff --git a/packages/flutter_tools/test/ios/xcodeproj_test.dart b/packages/flutter_tools/test/ios/xcodeproj_test.dart
index 75cec8a..426c9c4 100644
--- a/packages/flutter_tools/test/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/ios/xcodeproj_test.dart
@@ -1,9 +1,144 @@
-import 'package:test/test.dart';
+// Copyright 2018 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 '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/build_info.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import '../src/context.dart';
+
+const String xcodebuild = '/usr/bin/xcodebuild';
 
 void main() {
+  group('xcodebuild versioning', () {
+    MockProcessManager mockProcessManager;
+    XcodeProjectInterpreter xcodeProjectInterpreter;
+    FakePlatform macOS;
+    FileSystem fs;
+
+    setUp(() {
+      mockProcessManager = new MockProcessManager();
+      xcodeProjectInterpreter = new XcodeProjectInterpreter();
+      macOS = fakePlatform('macos');
+      fs = new MemoryFileSystem();
+      fs.file(xcodebuild).createSync(recursive: true);
+    });
+
+    void testUsingOsxContext(String description, dynamic testMethod()) {
+      testUsingContext(description, testMethod, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+      });
+    }
+
+    testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenThrow(const ProcessException(xcodebuild, const <String>['-version']));
+      expect(xcodeProjectInterpreter.versionText, isNull);
+    });
+
+    testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
+        new ProcessResult(
+          0,
+          1,
+          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
+          "but active developer directory '/Library/Developer/CommandLineTools' "
+          'is a command line tools instance',
+          '',
+        ),
+      );
+      expect(xcodeProjectInterpreter.versionText, isNull);
+    });
+
+    testUsingOsxContext('versionText returns formatted version text', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
+    });
+
+    testUsingOsxContext('versionText handles Xcode version string with unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
+    });
+
+    testUsingOsxContext('majorVersion returns major version', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.majorVersion, 8);
+    });
+
+    testUsingOsxContext('majorVersion is null when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.majorVersion, isNull);
+    });
+
+    testUsingOsxContext('minorVersion returns minor version', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, 3);
+    });
+
+    testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, 0);
+    });
+
+    testUsingOsxContext('minorVersion is null when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, isNull);
+    });
+
+    testUsingContext('isInstalled is false when not on MacOS', () {
+      fs.file(xcodebuild).deleteSync();
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    }, overrides: <Type, Generator>{
+      Platform: () => fakePlatform('notMacOS')
+    });
+
+    testUsingOsxContext('isInstalled is false when xcodebuild does not exist', () {
+      fs.file(xcodebuild).deleteSync();
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is false when Xcode is not fully installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
+        new ProcessResult(
+          0,
+          1,
+          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
+          "but active developer directory '/Library/Developer/CommandLineTools' "
+          'is a command line tools instance',
+          '',
+        ),
+      );
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is false when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is true when version has expected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.isInstalled, isTrue);
+    });
+  });
   group('Xcode project properties', () {
     test('properties from default project can be parsed', () {
       const String output = '''
@@ -118,3 +253,10 @@
     });
   });
 }
+
+Platform fakePlatform(String name) {
+  return new FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter { }
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 0a9f64d..01931b9 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -291,7 +291,16 @@
 
 class MockXcodeProjectInterpreter implements XcodeProjectInterpreter {
   @override
-  bool get canInterpretXcodeProjects => true;
+  bool get isInstalled => true;
+
+  @override
+  String get versionText => 'Xcode 9.2';
+
+  @override
+  int get majorVersion => 9;
+
+  @override
+  int get minorVersion => 2;
 
   @override
   Map<String, String> getBuildSettings(String projectPath, String target) {