Unverified Commit 7c75c01f authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Add recommended Xcode version to doctor (#73808)

parent c65304bb
......@@ -139,8 +139,9 @@ class UserMessages {
// Messages used in XcodeValidator
String xcodeLocation(String location) => 'Xcode at $location';
String xcodeOutdated(int versionMajor, int versionMinor, int versionPatch) =>
'Flutter requires a minimum Xcode version of $versionMajor.$versionMinor.$versionPatch.\n'
String xcodeOutdated(String currentVersion, String recommendedVersion) =>
'Xcode $currentVersion out of date ($recommendedVersion is recommended).\n'
'Download the latest version or update via the Mac App Store.';
String get xcodeEula => "Xcode end user license agreement not signed; open Xcode or run the command 'sudo xcodebuild -license'.";
String get xcodeMissingSimct =>
......
......@@ -637,7 +637,7 @@ class XcodeBuildExecution {
final Map<String, String> buildSettings;
}
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.$kXcodeRequiredVersionPatch or greater is required to develop for iOS.';
final String _xcodeRequirement = 'Xcode $xcodeRequiredVersion or greater is required to develop for iOS.';
bool _checkXcodeVersion() {
if (!globals.platform.isMacOS) {
......@@ -647,7 +647,7 @@ bool _checkXcodeVersion() {
globals.printError('Cannot find "xcodebuild". $_xcodeRequirement');
return false;
}
if (!globals.xcode.isVersionSatisfactory) {
if (!globals.xcode.isRequiredVersionSatisfactory) {
globals.printError('Found "${globals.xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
return false;
}
......
......@@ -6,6 +6,7 @@ import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/logger.dart';
import '../../base/project_migrator.dart';
import '../../base/version.dart';
import '../../macos/xcode.dart';
import '../../project.dart';
import '../../reporting/reporting.dart';
......@@ -99,7 +100,7 @@ class RemoveFrameworkLinkAndEmbeddingMigration extends ProjectMigrator {
if (line.contains('/* App.framework ') || line.contains('/* Flutter.framework ')) {
// Print scary message if the user is on Xcode 11.4 or greater, or if Xcode isn't installed.
final bool xcodeIsInstalled = _xcode.isInstalled;
if(!xcodeIsInstalled || (_xcode.majorVersion > 11 || (_xcode.majorVersion == 11 && _xcode.minorVersion >= 4))) {
if(!xcodeIsInstalled || _xcode.currentVersion >= Version(11, 4, 0)) {
UsageEvent('ios-migration', 'remove-frameworks', label: 'failure', flutterUsage: _usage).send();
throwToolExit('Your Xcode project requires migration. See https://flutter.dev/docs/development/ios-project-migration for details.');
}
......
......@@ -73,7 +73,6 @@ class IOSSimulatorUtils {
name: device.name,
simControl: _simControl,
simulatorCategory: device.category,
xcode: _xcode,
);
}).toList();
}
......@@ -309,9 +308,7 @@ class IOSSimulator extends Device {
this.name,
this.simulatorCategory,
@required SimControl simControl,
@required Xcode xcode,
}) : _simControl = simControl,
_xcode = xcode,
super(
id,
category: Category.mobile,
......@@ -325,7 +322,6 @@ class IOSSimulator extends Device {
final String simulatorCategory;
final SimControl _simControl;
final Xcode _xcode;
@override
DevFSWriter createDevFSWriter(covariant ApplicationPackage app, String userIdentifier) {
......@@ -610,12 +606,8 @@ class IOSSimulator extends Device {
}
}
bool get _xcodeVersionSupportsScreenshot {
return _xcode.majorVersion > 8 || (_xcode.majorVersion == 8 && _xcode.minorVersion >= 2);
}
@override
bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
bool get supportsScreenshot => true;
@override
Future<void> takeScreenshot(File outputFile) {
......
......@@ -14,6 +14,7 @@ import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
......@@ -25,9 +26,8 @@ import '../ios/mac.dart';
import '../ios/xcodeproj.dart';
import '../reporting/reporting.dart';
const int kXcodeRequiredVersionMajor = 11;
const int kXcodeRequiredVersionMinor = 0;
const int kXcodeRequiredVersionPatch = 0;
Version get xcodeRequiredVersion => Version(11, 0, 0, text: '11.0');
Version get xcodeRecommendedVersion => Version(12, 0, 1, text: '12.0.1');
/// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode
/// SUPPORTED_PLATFORMS values.
......@@ -60,7 +60,7 @@ class Xcode {
final FileSystem _fileSystem;
final XcodeProjectInterpreter _xcodeProjectInterpreter;
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isVersionSatisfactory;
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
String _xcodeSelectPath;
String get xcodeSelectPath {
......@@ -85,9 +85,13 @@ class Xcode {
return _xcodeProjectInterpreter.isInstalled;
}
int get majorVersion => _xcodeProjectInterpreter.majorVersion;
int get minorVersion => _xcodeProjectInterpreter.minorVersion;
int get patchVersion => _xcodeProjectInterpreter.patchVersion;
Version get currentVersion => Version(
_xcodeProjectInterpreter.majorVersion,
_xcodeProjectInterpreter.minorVersion,
_xcodeProjectInterpreter.patchVersion,
text:
'${_xcodeProjectInterpreter.majorVersion}.${_xcodeProjectInterpreter.minorVersion}.${_xcodeProjectInterpreter.patchVersion}',
);
String get versionText => _xcodeProjectInterpreter.versionText;
......@@ -132,20 +136,18 @@ class Xcode {
return _isSimctlInstalled;
}
bool get isVersionSatisfactory {
bool get isRequiredVersionSatisfactory {
if (!_xcodeProjectInterpreter.isInstalled) {
return false;
}
if (majorVersion > kXcodeRequiredVersionMajor) {
return true;
}
if (majorVersion == kXcodeRequiredVersionMajor) {
if (minorVersion == kXcodeRequiredVersionMinor) {
return patchVersion >= kXcodeRequiredVersionPatch;
}
return minorVersion >= kXcodeRequiredVersionMinor;
return currentVersion >= xcodeRequiredVersion;
}
bool get isRecommendedVersionSatisfactory {
if (!_xcodeProjectInterpreter.isInstalled) {
return false;
}
return false;
return currentVersion >= xcodeRecommendedVersion;
}
/// See [XcodeProjectInterpreter.xcrunCommand].
......
......@@ -29,18 +29,20 @@ class XcodeValidator extends DoctorValidator {
xcodeStatus = ValidationType.installed;
messages.add(ValidationMessage(_userMessages.xcodeLocation(_xcode.xcodeSelectPath)));
xcodeVersionInfo = _xcode.versionText;
if (xcodeVersionInfo.contains(',')) {
xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
}
messages.add(ValidationMessage(_xcode.versionText));
if (!_xcode.isInstalledAndMeetsVersionCheck) {
xcodeStatus = ValidationType.partial;
messages.add(ValidationMessage.error(
_userMessages.xcodeOutdated(kXcodeRequiredVersionMajor, kXcodeRequiredVersionMinor, kXcodeRequiredVersionPatch)
));
messages.add(ValidationMessage.error(_userMessages.xcodeOutdated(
_xcode.currentVersion.toString(),
xcodeRecommendedVersion.toString(),
)));
} else if (!_xcode.isRecommendedVersionSatisfactory) {
xcodeStatus = ValidationType.partial;
messages.add(ValidationMessage.hint(_userMessages.xcodeOutdated(
_xcode.currentVersion.toString(),
xcodeRecommendedVersion.toString(),
)));
}
if (!_xcode.eulaSigned) {
......
......@@ -101,7 +101,7 @@ void main() {
}
);
mockXcode = MockXcode();
when(mockXcode.isVersionSatisfactory).thenReturn(true);
when(mockXcode.isRequiredVersionSatisfactory).thenReturn(true);
when(mockXcode.xcrunCommand()).thenReturn(<String>['xcrun']);
fileSystem.file('foo/.packages')
..createSync(recursive: true)
......
......@@ -6,6 +6,7 @@ import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/project_migrator.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migration.dart';
import 'package:flutter_tools/src/ios/migrations/project_build_location_migration.dart';
import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart';
......@@ -145,8 +146,7 @@ keep this 2
746232531E83B71900CC1A5E /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 746232521E83B71900CC1A5E /* App.framework */; };
''');
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.majorVersion).thenReturn(11);
when(mockXcode.minorVersion).thenReturn(4);
when(mockXcode.currentVersion).thenReturn(Version(11, 4, 0));
final RemoveFrameworkLinkAndEmbeddingMigration iosProjectMigration = RemoveFrameworkLinkAndEmbeddingMigration(
mockIosProject,
......@@ -164,8 +164,7 @@ keep this 2
9705A1C71CF904A300538480 /* Flutter.framework in Embed Frameworks */,
''');
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.majorVersion).thenReturn(11);
when(mockXcode.minorVersion).thenReturn(4);
when(mockXcode.currentVersion).thenReturn(Version(11, 4, 0));
final RemoveFrameworkLinkAndEmbeddingMigration iosProjectMigration = RemoveFrameworkLinkAndEmbeddingMigration(
mockIosProject,
......@@ -198,8 +197,7 @@ keep this 2
746232531E83B71900CC1A5E /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 746232521E83B71900CC1A5E /* App.framework */; };
''');
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.majorVersion).thenReturn(11);
when(mockXcode.minorVersion).thenReturn(3);
when(mockXcode.currentVersion).thenReturn(Version(11, 3, 0));
final RemoveFrameworkLinkAndEmbeddingMigration iosProjectMigration = RemoveFrameworkLinkAndEmbeddingMigration(
mockIosProject,
......@@ -217,8 +215,7 @@ keep this 2
746232531E83B71900CC1A5E /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 746232521E83B71900CC1A5E /* App.framework */; };
''');
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.majorVersion).thenReturn(11);
when(mockXcode.minorVersion).thenReturn(4);
when(mockXcode.currentVersion).thenReturn(Version(11, 4, 0));
final RemoveFrameworkLinkAndEmbeddingMigration iosProjectMigration = RemoveFrameworkLinkAndEmbeddingMigration(
mockIosProject,
......@@ -235,8 +232,7 @@ keep this 2
746232531E83B71900CC1A5E /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 746232521E83B71900CC1A5E /* App.framework */; };
''');
when(mockXcode.isInstalled).thenReturn(true);
when(mockXcode.majorVersion).thenReturn(12);
when(mockXcode.minorVersion).thenReturn(0);
when(mockXcode.currentVersion).thenReturn(Version(12, 0, 0));
final RemoveFrameworkLinkAndEmbeddingMigration iosProjectMigration = RemoveFrameworkLinkAndEmbeddingMigration(
mockIosProject,
......
......@@ -221,13 +221,13 @@ void main() {
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isVersionSatisfactory, isFalse);
expect(xcode.isRequiredVersionSatisfactory, isFalse);
});
testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
expect(xcode.isVersionSatisfactory, isFalse);
expect(xcode.isRequiredVersionSatisfactory, isFalse);
});
testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () {
......@@ -236,7 +236,7 @@ void main() {
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isVersionSatisfactory, isTrue);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
......@@ -245,7 +245,7 @@ void main() {
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isVersionSatisfactory, isTrue);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
......@@ -254,7 +254,7 @@ void main() {
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isVersionSatisfactory, isTrue);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('xcodeVersionSatisfactory is true when patch version exceeds minimum', () {
......@@ -263,7 +263,58 @@ void main() {
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
expect(xcode.isVersionSatisfactory, isTrue);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('isRecommendedVersionSatisfactory 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.isRecommendedVersionSatisfactory, isFalse);
});
testWithoutContext('isRecommendedVersionSatisfactory is false when xcodebuild tools are not installed', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
expect(xcode.isRecommendedVersionSatisfactory, isFalse);
});
testWithoutContext('isRecommendedVersionSatisfactory is true when version meets minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('isRecommendedVersionSatisfactory is true when major version exceeds minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(13);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('isRecommendedVersionSatisfactory is true when minor version exceeds minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('isRecommendedVersionSatisfactory is true when patch version exceeds minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
when(mockXcodeProjectInterpreter.patchVersion).thenReturn(2);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/macos/xcode_validator.dart';
......@@ -40,12 +41,32 @@ void main() {
when(xcode.isInstalled).thenReturn(true);
when(xcode.versionText)
.thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
when(xcode.currentVersion).thenReturn(Version(7, 0, 1));
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
when(xcode.isRecommendedVersionSatisfactory).thenReturn(false);
when(xcode.eulaSigned).thenReturn(true);
when(xcode.isSimctlInstalled).thenReturn(true);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final ValidationResult result = await validator.validate();
expect(result.type, ValidationType.partial);
expect(result.messages.last.type, ValidationMessageType.error);
expect(result.messages.last.message, contains('Xcode 7.0.1 out of date (12.0.1 is recommended)'));
});
testWithoutContext('Emits partial status when Xcode below recommended version', () async {
when(xcode.isInstalled).thenReturn(true);
when(xcode.versionText)
.thenReturn('Xcode 11.0\nBuild version 11A420a\n');
when(xcode.currentVersion).thenReturn(Version(11, 0, 0));
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
when(xcode.isRecommendedVersionSatisfactory).thenReturn(false);
when(xcode.eulaSigned).thenReturn(true);
when(xcode.isSimctlInstalled).thenReturn(true);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
final ValidationResult result = await validator.validate();
expect(result.type, ValidationType.partial);
expect(result.messages.last.type, ValidationMessageType.hint);
expect(result.messages.last.message, contains('Xcode 11.0.0 out of date (12.0.1 is recommended)'));
});
testWithoutContext('Emits partial status when Xcode EULA not signed', () async {
......@@ -53,6 +74,7 @@ void main() {
when(xcode.versionText)
.thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
when(xcode.isRecommendedVersionSatisfactory).thenReturn(true);
when(xcode.eulaSigned).thenReturn(false);
when(xcode.isSimctlInstalled).thenReturn(true);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
......@@ -65,6 +87,7 @@ void main() {
when(xcode.versionText)
.thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
when(xcode.isRecommendedVersionSatisfactory).thenReturn(true);
when(xcode.eulaSigned).thenReturn(true);
when(xcode.isSimctlInstalled).thenReturn(false);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
......@@ -78,6 +101,7 @@ void main() {
when(xcode.versionText)
.thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
when(xcode.isRecommendedVersionSatisfactory).thenReturn(true);
when(xcode.eulaSigned).thenReturn(true);
when(xcode.isSimctlInstalled).thenReturn(true);
final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment