blob: 32dcf380998484617afca50dd260c1c76ecf1679 [file]
// 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:meta/meta.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../context/tool_context.dart';
import '../ios/xcodeproj.dart';
import '../xcode_project.dart';
Version get xcodeRequiredVersion => Version(15, 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(16, null, null);
/// SDK name passed to `xcrun --sdk`.
///
/// Corresponds to undocumented Xcode `SUPPORTED_PLATFORMS` values.
///
/// Usage:
///
/// ```text
/// xcrun [options] <tool name> ... arguments ...
/// ...
/// --sdk <sdk name> find the tool for the given SDK name.
/// ```
String getSDKNameForIOSEnvironmentType(EnvironmentType environmentType) {
return (environmentType == EnvironmentType.simulator)
? XcodeSdk.IPhoneSimulator.platformName
: XcodeSdk.IPhoneOS.platformName;
}
/// A utility class for interacting with Xcode command line tools.
class Xcode {
Xcode({required this._toolContext, required this._xcodeProjectInterpreter});
/// Create an [Xcode] for testing.
///
/// Defaults to a memory file system, fake platform,
/// buffer logger, and test [XcodeProjectInterpreter].
@visibleForTesting
factory Xcode.test({
required ToolContext toolContext,
XcodeProjectInterpreter? xcodeProjectInterpreter,
}) {
return Xcode(
toolContext: toolContext,
xcodeProjectInterpreter:
xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(toolContext: toolContext),
);
}
final ToolContext _toolContext;
final XcodeProjectInterpreter _xcodeProjectInterpreter;
String get flutterRoot => _toolContext.cache.flutterRoot;
bool get isInstalledAndMeetsVersionCheck =>
_toolContext.platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
String? _xcodeSelectPath;
String? get xcodeSelectPath {
if (_xcodeSelectPath == null) {
try {
_xcodeSelectPath = _toolContext.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 UserMessages userMessages = _toolContext.userMessages;
final String? pathToXcode = xcodeSelectPath;
if (pathToXcode == null || pathToXcode.isEmpty) {
throwToolExit(userMessages.xcodeMissing);
}
final int index = pathToXcode.indexOf('.app');
if (index == -1) {
throwToolExit(userMessages.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 FileSystem fs = _toolContext.fs;
final String flutterToolsAbsolutePath = fs.path.join(flutterRoot, 'packages', 'flutter_tools');
final filePath = '$flutterToolsAbsolutePath/bin/xcode_debug.js';
if (!fs.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 = _toolContext.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 {
// This command will error if additional components need to be installed in
// xcode 9.2 and above.
_isSimctlInstalled ??= _toolContext.processUtils.exitsHappySync(<String>[
...xcrunCommand(),
'simctl',
'list',
'devices',
'booted',
]);
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) {
if (currentVersion == null) {
_isDevicectlInstalled = false;
return _isDevicectlInstalled!;
}
_isDevicectlInstalled = _toolContext.processUtils.exitsHappySync(<String>[
...xcrunCommand(),
'devicectl',
'--version',
]);
}
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<List<String>> fetchDependenciesAndGenerateXcodebuildArgs(
XcodeBasedProject xcodeProject,
Directory buildDirectory, {
bool skipPackageValidation = true,
}) async => _xcodeProjectInterpreter.fetchDependenciesAndGenerateXcodebuildArgs(
xcodeProject,
buildDirectory,
skipPackageValidation: skipPackageValidation,
);
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 _toolContext.processUtils.run(<String>[
...xcrunCommand(),
command,
...args,
], throwOnError: true);
}
Future<String> sdkLocation(EnvironmentType environmentType) async {
final RunResult runResult = await _toolContext.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 FileSystem fs = _toolContext.fs;
final String deviceHubPath = fs.path.join(
fs.path.dirname(selectPath),
'Applications',
'DeviceHub.app',
);
if (fs.directory(deviceHubPath).existsSync()) {
return deviceHubPath;
}
final String simulatorPath = fs.path.join(selectPath, 'Applications', 'Simulator.app');
return fs.directory(simulatorPath).existsSync() ? simulatorPath : null;
}
/// Gets the version number of the platform for the selected SDK.
Future<Version?> sdkPlatformVersion(EnvironmentType environmentType) async {
final ToolContext(:Logger logger, :ProcessUtils processUtils) = _toolContext;
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);
}
}
/// Determines the iOS [EnvironmentType] from [sdkroot].
///
/// Throws a [ToolExit] for unrecognized or non-iOS SDK roots (e.g., `MacOSX.sdk`
/// or `XROS.sdk`).
EnvironmentType environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {
// NOTE: If you modify this function, you should likely also update the equivalent implementation in
// packages/flutter_tools/templates/add_to_app/darwin/Tools/FlutterToolHelper/FlutterToolHelper.swift.tmpl
// 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;
}
throwToolExit(
'Unsupported iOS SDK root "$sdkroot". Expected an iPhoneOS or iPhoneSimulator SDK. '
"Flutter only supports building the iOS Runner for iOS; check the target's Base SDK "
'(SDKROOT) and Supported Destinations in Xcode.',
);
}