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) {