Exclude arm64 if any dependencies do and print warning when using Xcode 26 (#182913)
If any Pod dependencies exclude the arm64 architecture for simulators,
Flutter excludes it too. This will force Xcode to build it in an x86
architecture. However, iOS 26 introduced arm-only simulators. So when
using Xcode 26, print a warning with the list of plugins/targets that
are excluding arm and direct them to request arm support. Then if the
build fails due to targeting an arm-only simulator, give a guided
message to switch to an x86 simulator.
Fixes https://github.com/flutter/flutter/issues/182748.
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 51d0329..390b53d 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -531,6 +531,7 @@
logger: globals.logger,
platform: FlutterDarwinPlatform.ios,
project: package.project.parent,
+ device: this,
);
_logger.printError('');
return LaunchResult.failed();
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 494260d..150ebe3 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -336,6 +336,7 @@
project: project,
targetOverride: targetOverride,
buildInfo: buildInfo,
+ printWarnings: true,
);
if (app.project.usesSwiftPackageManager) {
final String? iosDeploymentTarget = buildSettings['IPHONEOS_DEPLOYMENT_TARGET'];
@@ -818,6 +819,7 @@
required FileSystem fileSystem,
required FlutterDarwinPlatform platform,
required FlutterProject project,
+ Device? device,
}) async {
final XcodeBuildExecution? xcodeBuildExecution = result.xcodeBuildExecution;
if (xcodeBuildExecution != null &&
@@ -846,6 +848,7 @@
platform: platform,
logger: logger,
fileSystem: fileSystem,
+ device: device,
);
if (!issueDetected && xcodeBuildExecution != null) {
@@ -1035,6 +1038,19 @@
hasProvisioningProfileIssue: false,
modifiedPrecompiledSource: true,
);
+ } else if (message.contains(
+ 'Unable to find a destination matching the provided destination specifier',
+ ) &&
+ message.contains('platform:iOS Simulator, arch:x86_64,') &&
+ !message.contains('platform:iOS Simulator, arch:arm64,') &&
+ !message.contains(
+ 'The requested device could not be found because no available devices matched the request.',
+ )) {
+ return _XCResultIssueHandlingResult(
+ requiresProvisioningProfile: false,
+ hasProvisioningProfileIssue: false,
+ unableToFindArmDestination: true,
+ );
}
return _XCResultIssueHandlingResult(
requiresProvisioningProfile: false,
@@ -1050,11 +1066,13 @@
required FlutterDarwinPlatform platform,
required Logger logger,
required FileSystem fileSystem,
+ Device? device,
}) async {
var requiresProvisioningProfile = false;
var hasProvisioningProfileIssue = false;
var issueDetected = false;
var modifiedPrecompiledSource = false;
+ var unableToFindArmDestination = false;
String? missingPlatform;
final duplicateModules = <String>[];
final missingModules = <String>[];
@@ -1081,6 +1099,7 @@
missingModules.add(handlingResult.missingModule!);
}
modifiedPrecompiledSource = handlingResult.modifiedPrecompiledSource;
+ unableToFindArmDestination = handlingResult.unableToFindArmDestination;
issueDetected = true;
}
} else if (xcResult != null) {
@@ -1155,10 +1174,42 @@
'the cache.\n'
'════════════════════════════════════════════════════════════════════════════════',
);
+ } else if (unableToFindArmDestination &&
+ xcodeBuildExecution != null &&
+ xcodeBuildExecution.environmentType == EnvironmentType.simulator &&
+ device != null) {
+ final bool simulatorSupportsIntel = await _simulatorSupportsIntel(device);
+ if (!simulatorSupportsIntel) {
+ logger.printError(
+ '════════════════════════════════════════════════════════════════════════════════\n'
+ 'The selected simulator is incompatible with the current build settings.\n'
+ 'Please use a simulator that supports x86_64, such as a simulator prior to iOS 26 or '
+ 'download the universal variant of the iOS 26 simulator using '
+ '"xcodebuild -downloadPlatform iOS -architectureVariant universal".\n'
+ '════════════════════════════════════════════════════════════════════════════════',
+ );
+ }
}
return issueDetected;
}
+Future<bool> _simulatorSupportsIntel(Device device) async {
+ final Version? xcodeVersion = globals.xcode?.currentVersion;
+ if (xcodeVersion != null && xcodeVersion.major < 26) {
+ return true;
+ }
+ final String runtime = await device.sdkNameAndVersion;
+ final RunResult result = await globals.processUtils.run([
+ ...globals.xcode!.xcrunCommand(),
+ 'simctl',
+ 'list',
+ 'runtimes',
+ runtime,
+ '--json',
+ ]);
+ return result.stdout.contains('x86_64');
+}
+
/// Returns true if a Package.swift is found for the plugin and a podspec is not.
Future<bool> _isPluginSwiftPackageOnly({
required FlutterDarwinPlatform platform,
@@ -1293,6 +1344,7 @@
this.duplicateModule,
this.missingModule,
this.modifiedPrecompiledSource = false,
+ this.unableToFindArmDestination = false,
});
/// An issue indicates that user didn't provide the provisioning profile.
@@ -1314,6 +1366,8 @@
/// An issue indicates that a source file, such as a header in the Flutter framework, has
/// changed since last built. This requires "flutter clean" to resolve.
final bool modifiedPrecompiledSource;
+
+ final bool unableToFindArmDestination;
}
const _kResultBundlePath = 'temporary_xcresult_bundle';
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index fbdd11c..4af0bf2 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -559,6 +559,7 @@
logger: globals.logger,
platform: FlutterDarwinPlatform.ios,
project: app.project.parent,
+ device: this,
);
throwToolExit('Could not build the application for the simulator.');
}
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 f0ae8d3..f8cc056 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
@@ -5,7 +5,6 @@
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
-import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../flutter_manifest.dart';
@@ -36,6 +35,7 @@
bool useMacOSConfig = false,
String? buildDirOverride,
String? configurationBuildDir,
+ bool printWarnings = false,
}) async {
final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
project: project,
@@ -44,6 +44,7 @@
useMacOSConfig: useMacOSConfig,
buildDirOverride: buildDirOverride,
configurationBuildDir: configurationBuildDir,
+ printWarnings: printWarnings,
);
_updateGeneratedXcodePropertiesFile(
@@ -150,6 +151,7 @@
bool useMacOSConfig = false,
String? buildDirOverride,
String? configurationBuildDir,
+ required bool printWarnings,
}) async {
final xcodeBuildSettings = <String>[];
@@ -240,11 +242,10 @@
// 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.
- final Version? xcodeVersion = globals.xcode?.currentVersion;
- if (xcodeVersion != null && xcodeVersion.major >= 26) {
- await project.ios.checkForPluginsExcludingArmSimulator();
+ var excludedSimulatorArchs = 'i386';
+ if (!(await project.ios.pluginsSupportArmSimulator(printWarnings: printWarnings))) {
+ excludedSimulatorArchs += ' arm64';
}
- const excludedSimulatorArchs = 'i386';
xcodeBuildSettings.add(
'EXCLUDED_ARCHS[sdk=${XcodeSdk.IPhoneSimulator.platformName}*]=$excludedSimulatorArchs',
);
diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart
index 9d9d654..8d1751c 100644
--- a/packages/flutter_tools/lib/src/xcode_project.dart
+++ b/packages/flutter_tools/lib/src/xcode_project.dart
@@ -5,6 +5,8 @@
/// @docImport 'ios/mac.dart';
library;
+import 'package:yaml/yaml.dart' as yaml;
+
import 'base/error_handling_io.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
@@ -489,79 +491,207 @@
/// True if the app project uses Swift.
bool get isSwift => appDelegateSwift.existsSync();
- /// Prints a warning if any plugin(s) are excluding `arm64` architecture.
+ /// Returns true if all plugins and their dependencies support arm64.
///
- /// Xcode 26 no longer allows you to build x86-only architecture for the simulator
- Future<void> checkForPluginsExcludingArmSimulator() async {
+ /// When using Xcode 26+, print a warning if a plugin or its dependencies does not support
+ /// arm64.
+ Future<bool> pluginsSupportArmSimulator({required bool printWarnings}) async {
+ final Version? xcodeVersion = globals.xcode?.currentVersion;
final Directory podXcodeProject = hostAppRoot
.childDirectory('Pods')
.childDirectory('Pods.xcodeproj');
if (!podXcodeProject.existsSync()) {
- return;
+ return true;
}
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (xcodeProjectInterpreter == null) {
- return;
+ // Xcode isn't installed, don't try to check.
+ return true;
}
final String? buildSettings = await xcodeProjectInterpreter.pluginsBuildSettingsOutput(
podXcodeProject,
);
-
if (buildSettings == null || buildSettings.isEmpty) {
- return;
+ globals.logger.printTrace('Unable to get build settings for Pods.');
+ return true;
}
+ // When using Xcode 26, print a warning if a target does not support arm.
+ if (xcodeVersion != null && xcodeVersion.major >= 26 && printWarnings) {
+ final List<({String target, String? plugin})> targetsExcludingArm =
+ await _targetsExcludingArm(buildSettings);
+ if (targetsExcludingArm.isNotEmpty) {
+ final String list = targetsExcludingArm
+ .map((target) {
+ var targetItem = ' - ${target.target}';
+ if (target.target == target.plugin) {
+ targetItem = '$targetItem (Flutter plugin)';
+ } else if (target.plugin != null) {
+ targetItem =
+ '$targetItem (transitive dependency of Flutter plugin ${target.plugin})';
+ }
+ return targetItem;
+ })
+ .join('\n');
+ globals.logger.printWarning(
+ 'The following target(s) do not support arm64 architecture, which is a requirement for '
+ 'Apple Silicon iOS 26+ simulators:\n'
+ '$list\n\n'
+ 'Please contact plugin maintainers to request arm64 support to continue to be able to '
+ 'use the plugin on a simulator.',
+ );
+ }
+ }
+
+ return !buildSettings.contains(RegExp('EXCLUDED_ARCHS.*arm64'));
+ }
+
+ /// Returns a list of targets and their associated plugin (if found) that exclude arm64 architecture.
+ Future<List<({String target, String? plugin})>> _targetsExcludingArm(String buildSettings) async {
+ final Map<String, List<String>> cocoapodsDependencyGraph = _cocoapodsDependencyGraph();
final List<Plugin> allPlugins = await findPlugins(parent);
- final iosPluginTargetNames = <String>{
+ final pluginNames = <String>{
for (final Plugin plugin in allPlugins)
if (plugin.platforms.containsKey(IOSPlugin.kConfigKey)) plugin.name,
};
- if (iosPluginTargetNames.isEmpty) {
- return;
- }
-
- final targetHeader = RegExp(
+ final targetHeaderPattern = RegExp(
r'^Build settings for action build and target "?([^":\r\n]+)"?:\s*$',
);
- final pluginsExcludingArmArch = <String>{};
+ final List<({String target, String? plugin})> foundTargets = [];
String? currentTarget;
-
for (final String eachLine in buildSettings.split('\n')) {
final String settingsLine = eachLine.trim();
-
- final RegExpMatch? headerMatch = targetHeader.firstMatch(settingsLine);
+ final RegExpMatch? headerMatch = targetHeaderPattern.firstMatch(settingsLine);
if (headerMatch != null) {
currentTarget = headerMatch.group(1)!.trim();
continue;
}
-
- if (currentTarget == null || !iosPluginTargetNames.contains(currentTarget)) {
+ if (currentTarget == null ||
+ !settingsLine.startsWith('EXCLUDED_ARCHS') ||
+ !settingsLine.contains('=')) {
continue;
}
-
- if (!settingsLine.startsWith('EXCLUDED_ARCHS') || !settingsLine.contains('=')) {
- continue;
- }
-
final Iterable<String> tokens = settingsLine.split(' ');
- if (tokens.contains('arm64')) {
- pluginsExcludingArmArch.add(currentTarget);
+ if (!tokens.contains('arm64')) {
+ continue;
+ }
+
+ if (pluginNames.contains(currentTarget)) {
+ foundTargets.add((target: currentTarget, plugin: currentTarget));
+ } else {
+ // If it's not a plugin, it may be a transitive dependency of a plugin
+ final String? parentPlugin = _pluginUsingDependency(
+ targetName: currentTarget,
+ pluginNames: pluginNames.toList(),
+ cocoapodsDependencyGraph: cocoapodsDependencyGraph,
+ );
+ if (parentPlugin != null) {
+ foundTargets.add((target: currentTarget, plugin: parentPlugin));
+ } else if (currentTarget.startsWith('Pods-')) {
+ // Skip Pods- targets since they are not actual dependencies.
+ continue;
+ } else {
+ foundTargets.add((target: currentTarget, plugin: null));
+ }
}
}
+ return foundTargets;
+ }
- if (pluginsExcludingArmArch.isNotEmpty) {
- final String list = pluginsExcludingArmArch.map((String n) => ' - $n').join('\n');
-
- globals.logger.printWarning(
- 'The following plugin(s) are excluding the arm64 architecture, which is a requirement for Xcode 26+:\n'
- '$list\n'
- 'Consider installing the "Universal" Xcode or file an issue with the plugin(s) to support arm64.',
+ /// Returns the plugin that uses the given target.
+ String? _pluginUsingDependency({
+ required String targetName,
+ required List<String> pluginNames,
+ required Map<String, List<String>> cocoapodsDependencyGraph,
+ }) {
+ final String? pluginName = _findPluginForDependency(
+ targetName,
+ cocoapodsDependencyGraph,
+ pluginNames,
+ );
+ if (pluginName != null) {
+ return pluginName;
+ }
+ if (targetName.contains('-')) {
+ // Resource bundle targets are prefixed with the name of the pod and then the name of the
+ // resource. Strip the resource name to get the pod name.
+ return _findPluginForDependency(
+ targetName.split('-').first,
+ cocoapodsDependencyGraph,
+ pluginNames,
+ );
+ } else {
+ // Sometimes the target name is a prefix of pod name.
+ // Example: target name "GoogleMLKit" and pod name "GoogleMLKit/Translate".
+ return _findPluginForDependency(
+ targetName,
+ cocoapodsDependencyGraph,
+ pluginNames,
+ searchByPrefix: true,
);
}
}
+ /// Returns a map of pods and their dependencies.
+ Map<String, List<String>> _cocoapodsDependencyGraph() {
+ final Map<String, List<String>> podDependencies = {};
+ try {
+ if (podfileLock.existsSync()) {
+ final String podfileLockString = podfileLock.readAsStringSync().split('\n\n').first;
+ final dynamic podsYaml = yaml.loadYaml(podfileLockString);
+ if (podsYaml is yaml.YamlMap) {
+ final dynamic podsList = podsYaml['PODS'];
+ if (podsList is List<dynamic>) {
+ // ignore: specify_nonobvious_local_variable_types
+ for (final dynamic pod in podsList) {
+ if (pod is yaml.YamlMap) {
+ final name = pod.keys.first as String;
+ final dynamic value = pod.value[name];
+ if (value is yaml.YamlList) {
+ podDependencies[name.split(' ').first] = value
+ .map((dep) => (dep as String).split(' ').first)
+ .toList();
+ }
+ }
+ }
+ }
+ }
+ }
+ } on Exception catch (e) {
+ globals.logger.printTrace('Failed to parse podfile.lock: $e');
+ }
+
+ return podDependencies;
+ }
+
+ /// Recursively searches the pod dependency graph for a Flutter plugin that uses the given target.
+ String? _findPluginForDependency(
+ String targetName,
+ Map<String, List<String>> cocoapodTree,
+ List<String> pluginNames, {
+ bool searchByPrefix = false,
+ }) {
+ for (final MapEntry<String, List<String>> pod in cocoapodTree.entries) {
+ final String podName = pod.key;
+ final List<String> podDependencies = pod.value;
+ bool podHasTargetAsDependency;
+ if (searchByPrefix) {
+ podHasTargetAsDependency = podDependencies.any((dep) => dep.startsWith('$targetName/'));
+ } else {
+ podHasTargetAsDependency = podDependencies.contains(targetName);
+ }
+ if (podHasTargetAsDependency) {
+ if (pluginNames.contains(podName)) {
+ return podName;
+ }
+ return _findPluginForDependency(podName, cocoapodTree, pluginNames);
+ }
+ }
+ return null;
+ }
+
@override
bool existsSync() {
return parent.isModule || _editableDirectory.existsSync();
diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
index 8e1ec42..bd9e42c 100644
--- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
@@ -8,6 +8,7 @@
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
+import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
@@ -16,6 +17,7 @@
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/ios/code_signing.dart';
import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/ios/xcresult.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
@@ -720,6 +722,93 @@
),
);
});
+
+ group('parses unable to find simulator', () {
+ late FakeProcessManager processManager;
+ setUp(() {
+ processManager = FakeProcessManager.any();
+ });
+
+ testUsingContext(
+ 'on Xcode 26 if simulator is arm-only',
+ () async {
+ final fakeDevice = FakeDevice();
+ processManager = FakeProcessManager.list([
+ FakeCommand(
+ command: [
+ 'xcrun',
+ 'simctl',
+ 'list',
+ 'runtimes',
+ await fakeDevice.sdkNameAndVersion,
+ '--json',
+ ],
+ stdout: '''
+{
+ "runtimes" : [
+ {
+ "isAvailable" : true,
+ "version" : "26.0",
+ "isInternal" : false,
+ "buildversion" : "23A343",
+ "supportedArchitectures" : [
+ "arm64"
+ ],
+ ...
+ }
+ ]
+}''',
+ ),
+ ]);
+ const buildCommands = <String>['xcrun', 'cc', 'blah'];
+ final buildResult = XcodeBuildResult(
+ success: false,
+ stdout: '',
+ xcodeBuildExecution: XcodeBuildExecution(
+ buildCommands: buildCommands,
+ appDirectory: '/blah/blah',
+ environmentType: EnvironmentType.simulator,
+ buildSettings: buildSettings,
+ ),
+ xcResult: XCResult.test(
+ issues: <XCResultIssue>[
+ XCResultIssue.test(
+ message:
+ 'Unable to find a destination matching the provided destination specifier\n'
+ 'Available destinations for the "Runner" scheme:\n'
+ '{ platform:macOS, arch:arm64, variant:Designed for [iPad,iPhone], id:00006022-000868640E90A01E, name:My Mac }\n'
+ '{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }\n'
+ '{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device}\n'
+ '{ platform:iOS Simulator, arch:x86_64, id:12345678-1234-1234-1234-123456789012, OS:18.2, name:Flutter-iPhone }',
+ subType: 'Error',
+ ),
+ ],
+ ),
+ );
+ final fs = MemoryFileSystem.test();
+ final project = FakeFlutterProject(fileSystem: fs, usesSwiftPackageManager: true);
+ project.ios.podfile.createSync(recursive: true);
+ await diagnoseXcodeBuildFailure(
+ buildResult,
+ logger: logger,
+ analytics: fakeAnalytics,
+ fileSystem: fs,
+ platform: FlutterDarwinPlatform.ios,
+ project: project,
+ device: fakeDevice,
+ );
+ expect(
+ logger.errorText,
+ contains('The selected simulator is incompatible with the current build settings'),
+ );
+ expect(processManager.hasRemainingExpectations, isFalse);
+ },
+ overrides: <Type, Generator>{
+ XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(26, 0, 0)),
+ ProcessManager: () => processManager,
+ },
+ );
+ });
});
group('Upgrades project.pbxproj for old asset usage', () {
@@ -1008,3 +1097,40 @@
return frameworkPath;
}
}
+
+class FakeDevice extends Fake implements Device {
+ @override
+ String get name => 'My Mac';
+
+ @override
+ String get id => 'my-mac';
+
+ @override
+ Future<String> get sdkNameAndVersion async => 'com.apple.CoreSimulator.SimRuntime.iOS-26-2';
+}
+
+class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
+ FakeXcodeProjectInterpreter({
+ this.isInstalled = true,
+ this.version,
+ this.schemes = const <String>['Runner'],
+ });
+
+ @override
+ final bool isInstalled;
+
+ @override
+ final Version? version;
+
+ List<String> schemes;
+
+ @override
+ Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
+ return XcodeProjectInfo(<String>[], <String>[], schemes, BufferLogger.test());
+ }
+
+ @override
+ List<String> xcrunCommand() {
+ return ['xcrun'];
+ }
+}
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 58fc5eb..2738538 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -1188,7 +1188,7 @@
}
testUsingContext(
- 'prints Warning when a plugin excludes arm64 on Xcode 26+',
+ 'prints Warning when a plugin or transitive dependency excludes arm64 on Xcode 26+',
() async {
const BuildInfo buildInfo = BuildInfo.debug;
@@ -1230,22 +1230,64 @@
Build settings for action build and target bad_plugin:
EXCLUDED_ARCHS = i386 arm64
+Build settings for action build and target BadPluginPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target SecondPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target ThirdPodDependency-Sub:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target FourthPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
+
Build settings for action build and target good_plugin:
EXCLUDED_ARCHS = i386
''',
),
]);
- await updateGeneratedXcodeProperties(project: project, buildInfo: buildInfo);
+ fs.file('path/to/project/ios/Podfile.lock').writeAsStringSync('''
+PODS:
+ - good_plugin (1.0.0):
+ - Flutter
+ - bad_plugin (1.0.0):
+ - Flutter
+ - BadPluginPodDependency
+ - BadPluginPodDependency (1.0.0):
+ - SecondPodDependency/Sub
+ - SecondPodDependency/Sub (1.0.0):
+ - ThirdPodDependency
+ - ThirdPodDependency (1.0.0):
+ - FourthPodDependency (1.0.0):
+''');
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ printWarnings: true,
+ );
expect(
logger.warningText,
contains(
- 'The following plugin(s) are excluding the arm64 architecture, which is a requirement for Xcode 26+',
+ 'The following target(s) do not support arm64 architecture, which is a requirement for '
+ 'Apple Silicon iOS 26+ simulators:\n'
+ ' - bad_plugin (Flutter plugin)\n'
+ ' - BadPluginPodDependency (transitive dependency of Flutter plugin bad_plugin)\n'
+ ' - SecondPodDependency (transitive dependency of Flutter plugin bad_plugin)\n'
+ ' - ThirdPodDependency-Sub (transitive dependency of Flutter plugin bad_plugin)\n'
+ ' - FourthPodDependency\n',
),
);
- expect(logger.warningText, contains('bad_plugin'));
expect(fakeProcessManager, hasNoRemainingExpectations);
+
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(
+ config.readAsStringSync(),
+ contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64'),
+ );
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7'));
},
overrides: <Type, Generator>{
Artifacts: () => localIosArtifacts,
@@ -1257,8 +1299,9 @@
Logger: () => logger,
},
);
+
testUsingContext(
- 'succeeds when no plugins exclude arm64 on Xcode 26+',
+ 'does not exclude arm64 simulator when supported by all plugins',
() async {
const BuildInfo buildInfo = BuildInfo.debug;
@@ -1301,9 +1344,14 @@
),
]);
- await updateGeneratedXcodeProperties(project: project, buildInfo: buildInfo);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ printWarnings: true,
+ );
final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(logger.warningText, isEmpty);
expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386'));
expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7'));
expect(fakeProcessManager, hasNoRemainingExpectations);
@@ -1338,7 +1386,6 @@
createFakePlugins(project, fs, <String>['good_plugin']);
final String buildDirectory = fs.path.absolute('build', 'ios');
- final testLogger = BufferLogger.test();
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
@@ -1365,9 +1412,16 @@
),
]);
- await updateGeneratedXcodeProperties(project: project, buildInfo: buildInfo);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ printWarnings: true,
+ );
- expect(testLogger.warningText, isEmpty);
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(logger.warningText, isEmpty);
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386'));
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7'));
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
@@ -1427,16 +1481,24 @@
),
]);
- await updateGeneratedXcodeProperties(project: project, buildInfo: buildInfo);
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ printWarnings: true,
+ );
expect(
logger.warningText,
contains(
- 'The following plugin(s) are excluding the arm64 architecture, which is a requirement for Xcode 26+',
+ 'The following target(s) do not support arm64 architecture, which is a requirement for '
+ 'Apple Silicon iOS 26+ simulators:',
),
);
expect(logger.warningText, contains('bad_plugin'));
expect(fakeProcessManager, hasNoRemainingExpectations);
+ final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386'));
+ expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7'));
},
overrides: <Type, Generator>{
Artifacts: () => localIosArtifacts,
@@ -1450,8 +1512,16 @@
);
testUsingContext(
- 'ignores non-plugin targets that exclude arm64',
+ 'does not print warning on Xcode < 26',
() async {
+ xcodeProjectInterpreter = XcodeProjectInterpreter.test(
+ processManager: fakeProcessManager,
+ version: Version(16, 0, 0),
+ );
+ xcode = Xcode.test(
+ processManager: fakeProcessManager,
+ xcodeProjectInterpreter: xcodeProjectInterpreter,
+ );
const BuildInfo buildInfo = BuildInfo.debug;
final Directory projectDir = fs.directory('path/to/project')..createSync(recursive: true);
@@ -1466,10 +1536,9 @@
..createSync(recursive: true);
project.ios.podManifestLock.createSync(recursive: true);
- createFakePlugins(project, fs, <String>['good_plugin']);
+ createFakePlugins(project, fs, <String>['bad_plugin', 'good_plugin']);
final String buildDirectory = fs.path.absolute('build', 'ios');
- final testLogger = BufferLogger.test();
fakeProcessManager.addCommands(<FakeCommand>[
kWhichSysctlCommand,
@@ -1490,8 +1559,20 @@
'OBJROOT=$buildDirectory',
],
stdout: '''
-Build settings for action build and target SomeNonPluginTarget:
- EXCLUDED_ARCHS = arm64
+Build settings for action build and target bad_plugin:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target BadPluginPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target SecondPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target ThirdPodDependency-Sub:
+ EXCLUDED_ARCHS = i386 arm64
+
+Build settings for action build and target FourthPodDependency:
+ EXCLUDED_ARCHS = i386 arm64
Build settings for action build and target good_plugin:
EXCLUDED_ARCHS = i386
@@ -1499,49 +1580,35 @@
),
]);
- await updateGeneratedXcodeProperties(project: project, buildInfo: buildInfo);
+ fs.file('path/to/project/ios/Podfile.lock').writeAsStringSync('''
+PODS:
+ - good_plugin (1.0.0):
+ - Flutter
+ - bad_plugin (1.0.0):
+ - Flutter
+ - BadPluginPodDependency
+ - BadPluginPodDependency (1.0.0):
+ - SecondPodDependency/Sub
+ - SecondPodDependency/Sub (1.0.0):
+ - ThirdPodDependency
+ - ThirdPodDependency (1.0.0):
+ - FourthPodDependency (1.0.0):
+''');
+ await updateGeneratedXcodeProperties(
+ project: project,
+ buildInfo: buildInfo,
+ printWarnings: true,
+ );
- expect(testLogger.warningText, isEmpty);
+ expect(logger.warningText, isEmpty);
expect(fakeProcessManager, hasNoRemainingExpectations);
- },
- overrides: <Type, Generator>{
- Artifacts: () => localIosArtifacts,
- Platform: () => macOS,
- FileSystem: () => fs,
- ProcessManager: () => fakeProcessManager,
- XcodeProjectInterpreter: () => xcodeProjectInterpreter,
- Xcode: () => xcode,
- Logger: () => BufferLogger.test(),
- },
- );
-
- testUsingContext(
- 'succeeds and skips check on Xcode 16 even if a plugin excludes arm64',
- () async {
- xcodeProjectInterpreter = XcodeProjectInterpreter.test(
- processManager: fakeProcessManager,
- version: Version(16, 0, 0),
- );
- xcode = Xcode.test(
- processManager: fakeProcessManager,
- xcodeProjectInterpreter: xcodeProjectInterpreter,
- );
-
- const BuildInfo buildInfo = BuildInfo.debug;
- final FlutterProject project = FlutterProject.fromDirectoryTest(
- fs.directory('path/to/project'),
- );
- project.ios.hostAppRoot
- .childDirectory('Pods')
- .childDirectory('Pods.xcodeproj')
- .createSync(recursive: true);
-
- 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'));
+ expect(
+ config.readAsStringSync(),
+ contains('EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64'),
+ );
expect(config.readAsStringSync(), contains('EXCLUDED_ARCHS[sdk=iphoneos*]=armv7'));
- expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Artifacts: () => localIosArtifacts,