Ensure Xcode major, minor version always return non-null (#10980)

Previously, xcodeMajorVersion and xcodeMinorVersion returned null unless
xcodeVersionSatisfactory had been called first. We now compute them on
demand, and cache the resultant values.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 3c961e7..2e3661f 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -121,37 +121,48 @@
     return _eulaSigned;
   }
 
+  final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)');
+  void _updateXcodeVersion() {
+    try {
+      _xcodeVersionText = processManager.runSync(<String>['/usr/bin/xcodebuild', '-version']).stdout.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) {
-      try {
-        _xcodeVersionText = processManager.runSync(<String>['/usr/bin/xcodebuild', '-version']).stdout.replaceAll('\n', ', ');
-      } on ProcessException {
-        // Ignore: return null below.
-      }
-    }
+    if (_xcodeVersionText == null)
+      _updateXcodeVersion();
     return _xcodeVersionText;
   }
 
   int _xcodeMajorVersion;
-  int get xcodeMajorVersion => _xcodeMajorVersion;
+  int get xcodeMajorVersion {
+    if (_xcodeMajorVersion == null)
+      _updateXcodeVersion();
+    return _xcodeMajorVersion;
+  }
 
   int _xcodeMinorVersion;
-  int get xcodeMinorVersion => _xcodeMinorVersion;
-
-  final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)');
+  int get xcodeMinorVersion {
+    if (_xcodeMinorVersion == null)
+      _updateXcodeVersion();
+    return _xcodeMinorVersion;
+  }
 
   bool get xcodeVersionSatisfactory {
     if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText))
       return false;
-
-    final String version = xcodeVersionRegex.firstMatch(xcodeVersionText).group(1);
-    final List<String> components = version.split('.');
-
-    _xcodeMajorVersion = int.parse(components[0]);
-    _xcodeMinorVersion = components.length == 1 ? 0 : int.parse(components[1]);
-
-    return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion);
+    return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion);
   }
 
   Future<String> getAvailableDevices() async {
diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart
index 9313030..c23b708 100644
--- a/packages/flutter_tools/test/ios/mac_test.dart
+++ b/packages/flutter_tools/test/ios/mac_test.dart
@@ -116,6 +116,46 @@
       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('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.xcodeMinorVersion, isNull);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
     testUsingContext('eulaSigned is false when clang is not installed', () {
       when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
           .thenThrow(const ProcessException('/usr/bin/xcrun', const <String>['clang']));