Exclude arm64 from iOS app archs if unsupported by plugins (#87244)
diff --git a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
index c8b99b3..29a1d83 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
@@ -4,6 +4,7 @@
import '../artifacts.dart';
import '../base/file_system.dart';
+import '../base/os.dart';
import '../build_info.dart';
import '../cache.dart';
import '../flutter_manifest.dart';
@@ -35,7 +36,7 @@
bool useMacOSConfig = false,
String? buildDirOverride,
}) async {
- final List<String> xcodeBuildSettings = _xcodeBuildSettingsLines(
+ final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
project: project,
buildInfo: buildInfo,
targetOverride: targetOverride,
@@ -136,13 +137,13 @@
}
/// List of lines of build settings. Example: 'FLUTTER_BUILD_DIR=build'
-List<String> _xcodeBuildSettingsLines({
+Future<List<String>> _xcodeBuildSettingsLines({
required FlutterProject project,
required BuildInfo buildInfo,
String? targetOverride,
bool useMacOSConfig = false,
String? buildDirOverride,
-}) {
+}) async {
final List<String> xcodeBuildSettings = <String>[];
final String flutterRoot = globals.fs.path.normalize(Cache.flutterRoot!);
@@ -204,7 +205,15 @@
// ARM not yet supported https://github.com/flutter/flutter/issues/69221
xcodeBuildSettings.add('EXCLUDED_ARCHS=arm64');
} else {
- xcodeBuildSettings.add('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386');
+ String excludedSimulatorArchs = 'i386';
+
+ // If any plugins or their dependencies do not support arm64 simulators
+ // (to run natively without Rosetta translation on an ARM Mac),
+ // the app will fail to build unless it also excludes arm64 simulators.
+ if (globals.os.hostPlatform == HostPlatform.darwin_arm && !(await project.ios.pluginsSupportArmSimulator())) {
+ excludedSimulatorArchs += ' arm64';
+ }
+ xcodeBuildSettings.add('EXCLUDED_ARCHS[sdk=iphonesimulator*]=$excludedSimulatorArchs');
}
for (final MapEntry<String, String> config in buildInfo.toEnvironmentConfig().entries) {
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 5e48bf9..2e5d4bc 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -215,6 +215,58 @@
}
}
+ /// Asynchronously retrieve xcode build settings for the generated Pods.xcodeproj plugins project.
+ ///
+ /// Returns the stdout of the Xcode command.
+ Future<String?> pluginsBuildSettingsOutput(
+ Directory podXcodeProject, {
+ Duration timeout = const Duration(minutes: 1),
+ }) async {
+ if (!podXcodeProject.existsSync()) {
+ // No plugins.
+ return null;
+ }
+ final Status status = _logger.startSpinner();
+ final List<String> showBuildSettingsCommand = <String>[
+ ...xcrunCommand(),
+ 'xcodebuild',
+ '-alltargets',
+ '-sdk',
+ 'iphonesimulator',
+ '-project',
+ podXcodeProject.path,
+ '-showBuildSettings',
+ ];
+ try {
+ // showBuildSettings is reported to occasionally timeout. Here, we give it
+ // a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
+ // When there is a timeout, we retry once.
+ final RunResult result = await _processUtils.run(
+ showBuildSettingsCommand,
+ throwOnError: true,
+ workingDirectory: podXcodeProject.path,
+ timeout: timeout,
+ timeoutRetries: 1,
+ );
+
+ // Return the stdout only. Do not parse with parseXcodeBuildSettings, `-alltargets` prints the build settings
+ // for all targets (one per plugin), so it would require a Map of Maps.
+ return result.stdout.trim();
+ } on Exception catch (error) {
+ if (error is ProcessException && error.toString().contains('timed out')) {
+ BuildEvent('xcode-show-build-settings-timeout',
+ type: 'ios',
+ command: showBuildSettingsCommand.join(' '),
+ flutterUsage: _usage,
+ ).send();
+ }
+ _logger.printTrace('Unexpected failure to get Pod Xcode project build settings: $error.');
+ return null;
+ } finally {
+ status.stop();
+ }
+ }
+
Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async {
await _processUtils.run(<String>[
...xcrunCommand(),
diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart
index beb5f8a..36d4940 100644
--- a/packages/flutter_tools/lib/src/xcode_project.dart
+++ b/packages/flutter_tools/lib/src/xcode_project.dart
@@ -146,6 +146,30 @@
/// Xcode workspace shared workspace settings file for the host app.
File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');
+ /// Do all plugins support arm64 simulators to run natively on an ARM Mac?
+ Future<bool> pluginsSupportArmSimulator() async {
+ final Directory podXcodeProject = hostAppRoot
+ .childDirectory('Pods')
+ .childDirectory('Pods.xcodeproj');
+ if (!podXcodeProject.existsSync()) {
+ // No plugins.
+ return true;
+ }
+
+ final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
+ if (xcodeProjectInterpreter == null) {
+ // Xcode isn't installed, don't try to check.
+ return false;
+ }
+ final String? buildSettings = await xcodeProjectInterpreter.pluginsBuildSettingsOutput(podXcodeProject);
+
+ // See if any plugins or their dependencies exclude arm64 simulators
+ // as a valid architecture, usually because a binary is missing that slice.
+ // Example: EXCLUDED_ARCHS = arm64 i386
+ // NOT: EXCLUDED_ARCHS = i386
+ return buildSettings != null && !buildSettings.contains(RegExp('EXCLUDED_ARCHS.*arm64'));
+ }
+
@override
bool existsSync() {
return parent.isModule || _editableDirectory.existsSync();
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
index 07ae83d..1435cbe 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -9,6 +9,7 @@
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
@@ -20,6 +21,7 @@
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
+import '../../src/fakes.dart';
const String xcodebuild = '/usr/bin/xcodebuild';
@@ -38,7 +40,8 @@
],
);
- const FakeCommand kARMCheckCommand = FakeCommand(
+ // x64 host.
+ const FakeCommand kx64CheckCommand = FakeCommand(
command: <String>[
'sysctl',
'hw.optional.arm64',
@@ -46,6 +49,15 @@
exitCode: 1,
);
+ // ARM host.
+ const FakeCommand kARMCheckCommand = FakeCommand(
+ command: <String>[
+ 'sysctl',
+ 'hw.optional.arm64',
+ ],
+ stdout: 'hw.optional.arm64: 1',
+ );
+
FakeProcessManager fakeProcessManager;
XcodeProjectInterpreter xcodeProjectInterpreter;
FakePlatform platform;
@@ -70,7 +82,7 @@
testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
@@ -87,7 +99,7 @@
testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
exception: ProcessException(xcodebuild, <String>['-version']),
@@ -100,7 +112,7 @@
testWithoutContext('xcodebuild versionText returns formatted version text', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
@@ -114,7 +126,7 @@
testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
@@ -128,7 +140,7 @@
testWithoutContext('xcodebuild version parts can be parsed', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode 11.4.1\nBuild version 11N111s',
@@ -142,7 +154,7 @@
testWithoutContext('xcodebuild minor and patch version default to 0', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode 11\nBuild version 11N111s',
@@ -156,7 +168,7 @@
testWithoutContext('xcodebuild version parts is null when version has unexpected format', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
@@ -192,7 +204,7 @@
'xcodebuild isInstalled is false when Xcode is not fully installed', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, "
@@ -209,7 +221,7 @@
testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
@@ -223,7 +235,7 @@
testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-version'],
stdout: 'Xcode 8.3.3\nBuild version 8E3004b',
@@ -237,13 +249,7 @@
testWithoutContext('xcrun runs natively on arm64', () {
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- FakeCommand(
- command: <String>[
- 'sysctl',
- 'hw.optional.arm64',
- ],
- stdout: 'hw.optional.arm64: 1',
- ),
+ kARMCheckCommand,
]);
expect(xcodeProjectInterpreter.xcrunCommand(), <String>[
@@ -295,7 +301,7 @@
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
@@ -329,7 +335,7 @@
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
@@ -358,7 +364,7 @@
};
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
@@ -391,7 +397,7 @@
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>[
'xcrun',
@@ -416,7 +422,7 @@
const String workingDirectory = '/';
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-list'],
),
@@ -440,7 +446,7 @@
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-list'],
exitCode: 66,
@@ -466,7 +472,7 @@
fakeProcessManager.addCommands(const <FakeCommand>[
kWhichSysctlCommand,
- kARMCheckCommand,
+ kx64CheckCommand,
FakeCommand(
command: <String>['xcrun', 'xcodebuild', '-list'],
exitCode: 74,
@@ -676,10 +682,171 @@
fs.file(xcodebuild).createSync(recursive: true);
});
+ group('arm simulator', () {
+ FakeProcessManager fakeProcessManager;
+ XcodeProjectInterpreter xcodeProjectInterpreter;
+
+ setUp(() {
+ fakeProcessManager = FakeProcessManager.empty();
+ xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: fakeProcessManager);
+ });
+
+ testUsingContext('does not exclude arm64 simulator when supported by all plugins', () async {
+ const BuildInfo buildInfo = BuildInfo.debug;
+ final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+ final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
+ ..createSync(recursive: true);
+
+ fakeProcessManager.addCommands(<FakeCommand>[
+ kWhichSysctlCommand,
+ kARMCheckCommand,
+ FakeCommand(
+ command: <String>[
+ '/usr/bin/arch',
+ '-arm64e',
+ 'xcrun',
+ 'xcodebuild',
+ '-alltargets',
+ '-sdk',
+ 'iphonesimulator',
+ '-project',
+ podXcodeProject.path,
+ '-showBuildSettings',
+ ],
+ stdout: '''
+Build settings for action build and target plugin1:
+ ENABLE_BITCODE = NO;
+ EXCLUDED_ARCHS = i386;
+ INFOPLIST_FILE = Runner/Info.plist;
+ UNRELATED_BUILD_SETTING = arm64;
+
+Build settings for action build and target plugin2:
+ ENABLE_BITCODE = NO;
+ EXCLUDED_ARCHS = i386;
+ INFOPLIST_FILE = Runner/Info.plist;
+ UNRELATED_BUILD_SETTING = arm64;
+ '''
+ ),
+ ]);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ );
+
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386\n'));
+ expect(fakeProcessManager, hasNoRemainingExpectations);
+ }, overrides: <Type, Generator>{
+ Artifacts: () => localArtifacts,
+ Platform: () => macOS,
+ OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm),
+ FileSystem: () => fs,
+ ProcessManager: () => fakeProcessManager,
+ XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+ });
+
+ testUsingContext('excludes arm64 simulator when build setting fetch fails', () async {
+ const BuildInfo buildInfo = BuildInfo.debug;
+ final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+ final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
+ ..createSync(recursive: true);
+
+ fakeProcessManager.addCommands(<FakeCommand>[
+ kWhichSysctlCommand,
+ kARMCheckCommand,
+ FakeCommand(
+ command: <String>[
+ '/usr/bin/arch',
+ '-arm64e',
+ 'xcrun',
+ 'xcodebuild',
+ '-alltargets',
+ '-sdk',
+ 'iphonesimulator',
+ '-project',
+ podXcodeProject.path,
+ '-showBuildSettings',
+ ],
+ exitCode: 1,
+ ),
+ ]);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ );
+
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64\n'));
+ expect(fakeProcessManager, hasNoRemainingExpectations);
+ }, overrides: <Type, Generator>{
+ Artifacts: () => localArtifacts,
+ Platform: () => macOS,
+ OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm),
+ FileSystem: () => fs,
+ ProcessManager: () => fakeProcessManager,
+ XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+ });
+
+ testUsingContext('excludes arm64 simulator when unsupported by plugins', () async {
+ const BuildInfo buildInfo = BuildInfo.debug;
+ final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+ final Directory podXcodeProject = project.ios.hostAppRoot.childDirectory('Pods').childDirectory('Pods.xcodeproj')
+ ..createSync(recursive: true);
+
+ fakeProcessManager.addCommands(<FakeCommand>[
+ kWhichSysctlCommand,
+ kARMCheckCommand,
+ FakeCommand(
+ command: <String>[
+ '/usr/bin/arch',
+ '-arm64e',
+ 'xcrun',
+ 'xcodebuild',
+ '-alltargets',
+ '-sdk',
+ 'iphonesimulator',
+ '-project',
+ podXcodeProject.path,
+ '-showBuildSettings',
+ ],
+ stdout: '''
+Build settings for action build and target plugin1:
+ ENABLE_BITCODE = NO;
+ EXCLUDED_ARCHS = i386;
+ INFOPLIST_FILE = Runner/Info.plist;
+ UNRELATED_BUILD_SETTING = arm64;
+
+Build settings for action build and target plugin2:
+ ENABLE_BITCODE = NO;
+ EXCLUDED_ARCHS = i386 arm64;
+ INFOPLIST_FILE = Runner/Info.plist;
+ UNRELATED_BUILD_SETTING = arm64;
+ '''
+ ),
+ ]);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ );
+
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64\n'));
+ expect(fakeProcessManager, hasNoRemainingExpectations);
+ }, overrides: <Type, Generator>{
+ Artifacts: () => localArtifacts,
+ Platform: () => macOS,
+ OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm),
+ FileSystem: () => fs,
+ ProcessManager: () => fakeProcessManager,
+ XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+ });
+ });
+
void testUsingOsxContext(String description, dynamic Function() testMethod) {
testUsingContext(description, testMethod, overrides: <Type, Generator>{
Artifacts: () => localArtifacts,
Platform: () => macOS,
+ OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64),
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
@@ -708,6 +875,21 @@
expect(buildPhaseScriptContents.contains('EXCLUDED_ARCHS'), isFalse);
});
+ testUsingOsxContext('excludes i386 simulator', () async {
+ const BuildInfo buildInfo = BuildInfo.debug;
+ final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ );
+
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386\n'));
+
+ final File buildPhaseScript = fs.file('path/to/project/ios/Flutter/flutter_export_environment.sh');
+ expect(buildPhaseScript.readAsStringSync(), isNot(contains('EXCLUDED_ARCHS')));
+ });
+
testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, treeShakeIcons: false);
final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 3c96f3d..ebbcab54 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -307,6 +307,14 @@
}
@override
+ Future<String> pluginsBuildSettingsOutput(
+ Directory podXcodeProject, {
+ Duration timeout = const Duration(minutes: 1),
+ }) async {
+ return null;
+ }
+
+ @override
Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) {
return null;
}