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 c8b99b381b..29a1d83beb 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 @@ Future updateGeneratedXcodeProperties({ bool useMacOSConfig = false, String? buildDirOverride, }) async { - final List xcodeBuildSettings = _xcodeBuildSettingsLines( + final List xcodeBuildSettings = await _xcodeBuildSettingsLines( project: project, buildInfo: buildInfo, targetOverride: targetOverride, @@ -136,13 +137,13 @@ String? parsedBuildNumber({ } /// List of lines of build settings. Example: 'FLUTTER_BUILD_DIR=build' -List _xcodeBuildSettingsLines({ +Future> _xcodeBuildSettingsLines({ required FlutterProject project, required BuildInfo buildInfo, String? targetOverride, bool useMacOSConfig = false, String? buildDirOverride, -}) { +}) async { final List xcodeBuildSettings = []; final String flutterRoot = globals.fs.path.normalize(Cache.flutterRoot!); @@ -204,7 +205,15 @@ List _xcodeBuildSettingsLines({ // 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 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 5e48bf93d0..2e5d4bcd35 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -215,6 +215,58 @@ class XcodeProjectInterpreter { } } + /// Asynchronously retrieve xcode build settings for the generated Pods.xcodeproj plugins project. + /// + /// Returns the stdout of the Xcode command. + Future pluginsBuildSettingsOutput( + Directory podXcodeProject, { + Duration timeout = const Duration(minutes: 1), + }) async { + if (!podXcodeProject.existsSync()) { + // No plugins. + return null; + } + final Status status = _logger.startSpinner(); + final List showBuildSettingsCommand = [ + ...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 cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async { await _processUtils.run([ ...xcrunCommand(), diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index beb5f8a40b..36d4940178 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -146,6 +146,30 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { /// 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 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 07ae83d605..1435cbe18f 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/artifacts.dart'; 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 'package:flutter_tools/src/reporting/reporting.dart'; 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 @@ void main() { ], ); - const FakeCommand kARMCheckCommand = FakeCommand( + // x64 host. + const FakeCommand kx64CheckCommand = FakeCommand( command: [ 'sysctl', 'hw.optional.arm64', @@ -46,6 +49,15 @@ void main() { exitCode: 1, ); + // ARM host. + const FakeCommand kARMCheckCommand = FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + stdout: 'hw.optional.arm64: 1', + ); + FakeProcessManager fakeProcessManager; XcodeProjectInterpreter xcodeProjectInterpreter; FakePlatform platform; @@ -70,7 +82,7 @@ void main() { testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, " @@ -87,7 +99,7 @@ void main() { testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], exception: ProcessException(xcodebuild, ['-version']), @@ -100,7 +112,7 @@ void main() { testWithoutContext('xcodebuild versionText returns formatted version text', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode 8.3.3\nBuild version 8E3004b', @@ -114,7 +126,7 @@ void main() { testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode Ultra5000\nBuild version 8E3004b', @@ -128,7 +140,7 @@ void main() { testWithoutContext('xcodebuild version parts can be parsed', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode 11.4.1\nBuild version 11N111s', @@ -142,7 +154,7 @@ void main() { testWithoutContext('xcodebuild minor and patch version default to 0', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode 11\nBuild version 11N111s', @@ -156,7 +168,7 @@ void main() { testWithoutContext('xcodebuild version parts is null when version has unexpected format', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode Ultra5000\nBuild version 8E3004b', @@ -192,7 +204,7 @@ void main() { 'xcodebuild isInstalled is false when Xcode is not fully installed', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, " @@ -209,7 +221,7 @@ void main() { testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode Ultra5000\nBuild version 8E3004b', @@ -223,7 +235,7 @@ void main() { testWithoutContext('xcodebuild isInstalled is true when version has expected format', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-version'], stdout: 'Xcode 8.3.3\nBuild version 8E3004b', @@ -237,13 +249,7 @@ void main() { testWithoutContext('xcrun runs natively on arm64', () { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - FakeCommand( - command: [ - 'sysctl', - 'hw.optional.arm64', - ], - stdout: 'hw.optional.arm64: 1', - ), + kARMCheckCommand, ]); expect(xcodeProjectInterpreter.xcrunCommand(), [ @@ -295,7 +301,7 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: [ 'xcrun', @@ -329,7 +335,7 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: [ 'xcrun', @@ -358,7 +364,7 @@ void main() { }; fakeProcessManager.addCommands([ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: [ 'xcrun', @@ -391,7 +397,7 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: [ 'xcrun', @@ -416,7 +422,7 @@ void main() { const String workingDirectory = '/'; fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-list'], ), @@ -440,7 +446,7 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-list'], exitCode: 66, @@ -466,7 +472,7 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, - kARMCheckCommand, + kx64CheckCommand, FakeCommand( command: ['xcrun', 'xcodebuild', '-list'], exitCode: 74, @@ -676,10 +682,171 @@ Information about project "Runner": 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([ + kWhichSysctlCommand, + kARMCheckCommand, + FakeCommand( + command: [ + '/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: { + 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([ + kWhichSysctlCommand, + kARMCheckCommand, + FakeCommand( + command: [ + '/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: { + 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([ + kWhichSysctlCommand, + kARMCheckCommand, + FakeCommand( + command: [ + '/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: { + 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: { Artifacts: () => localArtifacts, Platform: () => macOS, + OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64), FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); @@ -708,6 +875,21 @@ Information about project "Runner": 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 3c96f3d93b..ebbcab54bc 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -306,6 +306,14 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { return {}; } + @override + Future pluginsBuildSettingsOutput( + Directory podXcodeProject, { + Duration timeout = const Duration(minutes: 1), + }) async { + return null; + } + @override Future cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) { return null;