Jenn Magder a5fea8c795
Increase the recommended Xcode version to Xcode 15 (#146367)
Xcode 15 will be required for iOS App Store submission 
> Please note that as of April 2024 all iOS and iPadOS apps submitted to the App Store must be built with a minimum of Xcode 15 and the iOS 17 SDK.

https://developer.apple.com/ios/submit/

And will also be required for Swift Package Manager support https://github.com/flutter/flutter/pull/146256.

We could swap to "required" but macOS developers don't technically need to upgrade.  We can let the Store itself enforce its policies.  And we can swap to Xcode 15 "required" when SPM adoption is further along.

Part of https://github.com/flutter/flutter/issues/144582
2024-04-08 16:56:06 +00:00

294 lines
9.4 KiB
Dart

// Copyright 2014 The Flutter 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 'dart:async';
import 'package:file/memory.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../ios/xcodeproj.dart';
Version get xcodeRequiredVersion => Version(14, null, null);
/// Diverging this number from the minimum required version will provide a doctor
/// warning, not error, that users should upgrade Xcode.
Version get xcodeRecommendedVersion => Version(15, null, null);
/// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode
/// SUPPORTED_PLATFORMS values.
///
/// Usage: xcrun [options] <tool name> ... arguments ...
/// ...
/// --sdk <sdk name> find the tool for the given SDK name.
String getSDKNameForIOSEnvironmentType(EnvironmentType environmentType) {
return (environmentType == EnvironmentType.simulator)
? 'iphonesimulator'
: 'iphoneos';
}
/// A utility class for interacting with Xcode command line tools.
class Xcode {
Xcode({
required Platform platform,
required ProcessManager processManager,
required Logger logger,
required FileSystem fileSystem,
required XcodeProjectInterpreter xcodeProjectInterpreter,
required UserMessages userMessages,
String? flutterRoot,
}) : _platform = platform,
_fileSystem = fileSystem,
_xcodeProjectInterpreter = xcodeProjectInterpreter,
_userMessage = userMessages,
_flutterRoot = flutterRoot,
_processUtils =
ProcessUtils(logger: logger, processManager: processManager),
_logger = logger;
/// Create an [Xcode] for testing.
///
/// Defaults to a memory file system, fake platform,
/// buffer logger, and test [XcodeProjectInterpreter].
@visibleForTesting
factory Xcode.test({
required ProcessManager processManager,
XcodeProjectInterpreter? xcodeProjectInterpreter,
Platform? platform,
FileSystem? fileSystem,
String? flutterRoot,
Logger? logger,
}) {
platform ??= FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{},
);
logger ??= BufferLogger.test();
return Xcode(
platform: platform,
processManager: processManager,
fileSystem: fileSystem ?? MemoryFileSystem.test(),
userMessages: UserMessages(),
flutterRoot: flutterRoot,
logger: logger,
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
);
}
final Platform _platform;
final ProcessUtils _processUtils;
final FileSystem _fileSystem;
final XcodeProjectInterpreter _xcodeProjectInterpreter;
final UserMessages _userMessage;
final String? _flutterRoot;
final Logger _logger;
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
String? _xcodeSelectPath;
String? get xcodeSelectPath {
if (_xcodeSelectPath == null) {
try {
_xcodeSelectPath = _processUtils.runSync(
<String>['/usr/bin/xcode-select', '--print-path'],
).stdout.trim();
} on ProcessException {
// Ignored, return null below.
} on ArgumentError {
// Ignored, return null below.
}
}
return _xcodeSelectPath;
}
String get xcodeAppPath {
// If the Xcode Select Path is /Applications/Xcode.app/Contents/Developer,
// the path to Xcode App is /Applications/Xcode.app
final String? pathToXcode = xcodeSelectPath;
if (pathToXcode == null || pathToXcode.isEmpty) {
throwToolExit(_userMessage.xcodeMissing);
}
final int index = pathToXcode.indexOf('.app');
if (index == -1) {
throwToolExit(_userMessage.xcodeMissing);
}
return pathToXcode.substring(0, index + 4);
}
/// Path to script to automate debugging through Xcode. Used in xcode_debug.dart.
/// Located in this file to make it easily overrideable in google3.
String get xcodeAutomationScriptPath {
final String flutterRoot = _flutterRoot ?? Cache.flutterRoot!;
final String flutterToolsAbsolutePath = _fileSystem.path.join(
flutterRoot,
'packages',
'flutter_tools',
);
final String filePath = '$flutterToolsAbsolutePath/bin/xcode_debug.js';
if (!_fileSystem.file(filePath).existsSync()) {
throwToolExit('Unable to find Xcode automation script at $filePath');
}
return filePath;
}
bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
Version? get currentVersion => _xcodeProjectInterpreter.version;
String? get buildVersion => _xcodeProjectInterpreter.build;
String? get versionText => _xcodeProjectInterpreter.versionText;
bool? _eulaSigned;
/// Has the EULA been signed?
bool get eulaSigned {
if (_eulaSigned == null) {
try {
final RunResult result = _processUtils.runSync(
<String>[...xcrunCommand(), 'clang'],
);
if (result.stdout.contains('license')) {
_eulaSigned = false;
} else if (result.stderr.contains('license')) {
_eulaSigned = false;
} else {
_eulaSigned = true;
}
} on ProcessException {
_eulaSigned = false;
}
}
return _eulaSigned ?? false;
}
bool? _isSimctlInstalled;
/// Verifies that simctl is installed by trying to run it.
bool get isSimctlInstalled {
if (_isSimctlInstalled == null) {
try {
// This command will error if additional components need to be installed in
// xcode 9.2 and above.
final RunResult result = _processUtils.runSync(
<String>[...xcrunCommand(), 'simctl', 'list', 'devices', 'booted'],
);
_isSimctlInstalled = result.exitCode == 0;
} on ProcessException {
_isSimctlInstalled = false;
}
}
return _isSimctlInstalled ?? false;
}
bool? _isDevicectlInstalled;
/// Verifies that `devicectl` is installed by checking Xcode version and trying
/// to run it. `devicectl` is made available in Xcode 15.
bool get isDevicectlInstalled {
if (_isDevicectlInstalled == null) {
try {
if (currentVersion == null || currentVersion!.major < 15) {
_isDevicectlInstalled = false;
return _isDevicectlInstalled!;
}
final RunResult result = _processUtils.runSync(
<String>[...xcrunCommand(), 'devicectl', '--version'],
);
_isDevicectlInstalled = result.exitCode == 0;
} on ProcessException {
_isDevicectlInstalled = false;
}
}
return _isDevicectlInstalled ?? false;
}
bool get isRequiredVersionSatisfactory {
final Version? version = currentVersion;
if (version == null) {
return false;
}
return version >= xcodeRequiredVersion;
}
bool get isRecommendedVersionSatisfactory {
final Version? version = currentVersion;
if (version == null) {
return false;
}
return version >= xcodeRecommendedVersion;
}
/// See [XcodeProjectInterpreter.xcrunCommand].
List<String> xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand();
Future<RunResult> cc(List<String> args) => _run('cc', args);
Future<RunResult> clang(List<String> args) => _run('clang', args);
Future<RunResult> dsymutil(List<String> args) => _run('dsymutil', args);
Future<RunResult> strip(List<String> args) => _run('strip', args);
Future<RunResult> _run(String command, List<String> args) {
return _processUtils.run(
<String>[...xcrunCommand(), command, ...args],
throwOnError: true,
);
}
Future<String> sdkLocation(EnvironmentType environmentType) async {
final RunResult runResult = await _processUtils.run(
<String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-path'],
);
if (runResult.exitCode != 0) {
throwToolExit('Could not find SDK location: ${runResult.stderr}');
}
return runResult.stdout.trim();
}
String? getSimulatorPath() {
final String? selectPath = xcodeSelectPath;
if (selectPath == null) {
return null;
}
final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app');
return _fileSystem.directory(appPath).existsSync() ? appPath : null;
}
/// Gets the version number of the platform for the selected SDK.
Future<Version?> sdkPlatformVersion(EnvironmentType environmentType) async {
final RunResult runResult = await _processUtils.run(
<String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-platform-version'],
);
if (runResult.exitCode != 0) {
_logger.printError('Could not find SDK Platform Version: ${runResult.stderr}');
return null;
}
final String versionString = runResult.stdout.trim();
return Version.parse(versionString);
}
}
EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {
// iPhoneSimulator.sdk or iPhoneOS.sdk
final String sdkName = fileSystem.path.basename(sdkroot).toLowerCase();
if (sdkName.contains('iphone')) {
return sdkName.contains('simulator') ? EnvironmentType.simulator : EnvironmentType.physical;
}
assert(false);
return null;
}