Skip to content

Commit ea208f8

Browse files
authored
Fix flutter run on Mac x64 hosts if Swift Package Manager is enabled (flutter#154645)
### Problem Enabling the Swift Package Manager feature caused post-submit tests to fail on Mac x64 hosts: <details> <summary>Example error...</summary> https://ci.chromium.org/ui/p/flutter/builders/prod/Mac_ios%20rrect_blur_perf_ios__timeline_summary/575/overview ``` â�¦ ... flutter --verbose assemble ... -dIosArchs=x86_64 ... profile_unpack_ios Target profile_unpack_ios failed: Exception: Binary ... build/ios/Profile-iphoneos/Flutter.framework/Flutter does not contain x86_64. Running lipo -info: Non-fat file: ... build/ios/Profile-iphoneos/Flutter.framework/Flutter is architecture: arm64 #0 UnpackIOS._thinFramework (package:flutter_tools/src/build_system/targets/ios.dart:351:7) <asynchronous suspension> #1 UnpackIOS.build (package:flutter_tools/src/build_system/targets/ios.dart:298:5) <asynchronous suspension> ... ``` </details> ### Reproduction On a mac x64 host: 1. Switch to the latest master channel: `flutter channel master ; flutter upgrade` 1. Disable the Swift Package Manager feature: `flutter config --no-enable-swift-package-manager` 2. Create a Flutter project 2. [Edit the Xcode project manually to add the prepare pre-action](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#step-2-add-run-prepare-flutter-framework-script-pre-action) 3. Run `flutter run` (`flutter build ios` does not reproduce this issue). ### Background Previously, the Flutter framework was unpacked in the Xcode target's build. Unfortunately, this happens after Swift packages are built; this prevented Swift packages from using the Flutter framework. To fix this, we added an Xcode pre-action that unpacks the Flutter framework _before_ Swift Package Manager builds packages. The Xcode target still runs the Flutter framework unpack step, but this step no-ops if the unpack step has the exact same inputs. ```mermaid flowchart LR A[flutter run -d iphone] --> B(Build Xcode project) B --> C(Xcode 'prepare framework' pre-action) B --> G[Build Swift packages] B --> D(Build 'Runner' target) C --> E[Unpack Flutter framework #1] D --> F[" Unpack Flutter framework #2 (No-ops if inputs are same as #1) "] ``` flutter#150052 added an optimization that made it more likely the second unpack step no-ops by fixing a case where the target architecture input could be different: > When using SwiftPM, we use `flutter assemble` in an Xcode Pre-action to run the `debug_unpack_macos` (or profile/release) target. This target is also later used in a Run Script build phase. Depending on `ARCHS` build setting, the Flutter/FlutterMacOS binary is thinned. In the Run Script build phase, `ARCHS` is filtered to the active arch. However, in the Pre-action it doesn't always filter to the active arch. As a workaround, assume arm64 if the [`NATIVE_ARCH`](https://developer.apple.com/documentation/xcode/build-settings-reference/#NATIVEARCH) is arm, otherwise assume x86_64. This optimization is only applied if [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157) is `YES`. > [!IMPORTANT] > [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157)'s name is misleading. It specifies whether the product includes only object code for the native architecture. > > A value of `YES` means the product includes only code for the native architecture ([NATIVE_ARCH](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW59)). > > A value of `NO` means the product includes code for the architectures specified in [ARCHS (Architectures)](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW62). ### Problem `buildXcodeProject` incorrectly always sets `ONLY_ACTIVE_ARCH` to `YES` if the Xcode built is for a single architecture: https://github.com/flutter/flutter/blob/6abef222514e8039bde5c47ab7864abbc4caf7e8/packages/flutter_tools/lib/src/ios/mac.dart#L353-L361 This is incorrect! If the host architecture is `x64` but the target architecture is `arm64`, [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157) should be `NO`. This caused the prepare pre-action to incorrectly use x64 as the target architecture for arm64 devices on an x64 host, which in turn caused builds to fail if Swift Package Manager was enabled. ### Solution This change updates `buildXcodeProject` to set `ONLY_ACTIVE_ARCH` correctly. This change also updates the prepare pre-action's to be more conservative in applying the optimization that filters the target architecture. This ensures that the build still works (but without the optimization) if `ONLY_ACTIVE_ARCH` is incorrectly set. Follow-up PR: flutter#154649 This unblocks: flutter#151567 ### DeviceLab test This problem reproduces if you `flutter run` to an iPhone Arm64 device from an x64 mac host with the Swift Package Manager feature enabled. I ran an affected DeviceLab test to verify the fix works as expected: Description | CI test | Result -- | -- | -- SwiftPM enabled without this fix: flutter#154750 | [Link](https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac_ios%20rrect_blur_perf_ios__timeline_summary/7/overview) | â�� SwiftPM enabled with this fix: flutter#154749 | [Link](https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac_ios%20rrect_blur_perf_ios__timeline_summary/8/overview) | â�
1 parent 3c2e25d commit ea208f8

File tree

6 files changed

+200
-20
lines changed

6 files changed

+200
-20
lines changed

packages/flutter_tools/bin/xcode_backend.dart

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -461,18 +461,20 @@ class Context {
461461
flutterArgs.add('--local-engine-host=${environment['LOCAL_ENGINE_HOST']}');
462462
}
463463

464-
String architectures = environment['ARCHS'] ?? '';
465-
if (command == 'prepare') {
466-
// The "prepare" command runs in a pre-action script, which doesn't always
467-
// filter the "ARCHS" build setting to only the active arch. To workaround,
468-
// if "ONLY_ACTIVE_ARCH" is true and the "NATIVE_ARCH" is arm, assume the
469-
// active arch is also arm to improve caching. If this assumption is
470-
// incorrect, it will later be corrected by the "build" command.
471-
if (environment['ONLY_ACTIVE_ARCH'] == 'YES' && environment['NATIVE_ARCH'] != null) {
472-
if (environment['NATIVE_ARCH']!.contains('arm')) {
473-
architectures = 'arm64';
474-
} else {
475-
architectures = 'x86_64';
464+
// The "prepare" command runs in a pre-action script, which doesn't always
465+
// filter the "ARCHS" build setting. Attempt to filter the architecture
466+
// to improve caching. If this filter is incorrect, it will later be
467+
// corrected by the "build" command.
468+
String archs = environment['ARCHS'] ?? '';
469+
if (command == 'prepare' && archs.contains(' ')) {
470+
// If "ONLY_ACTIVE_ARCH" is "YES", the product includes only code for the
471+
// native architecture ("NATIVE_ARCH").
472+
final String? nativeArch = environment['NATIVE_ARCH'];
473+
if (environment['ONLY_ACTIVE_ARCH'] == 'YES' && nativeArch != null) {
474+
if (nativeArch.contains('arm64') && archs.contains('arm64')) {
475+
archs = 'arm64';
476+
} else if (nativeArch.contains('x86_64') && archs.contains('x86_64')) {
477+
archs = 'x86_64';
476478
}
477479
}
478480
}
@@ -485,7 +487,7 @@ class Context {
485487
'-dTargetFile=$targetPath',
486488
'-dBuildMode=$buildMode',
487489
if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}',
488-
'-dIosArchs=$architectures',
490+
'-dIosArchs=$archs',
489491
'-dSdkRoot=${environment['SDKROOT'] ?? ''}',
490492
'-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',
491493
'-dTreeShakeIcons=${environment['TREE_SHAKE_ICONS'] ?? ''}',

packages/flutter_tools/lib/src/artifacts.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,17 @@ class CachedArtifacts implements Artifacts {
648648
switch (artifact) {
649649
case Artifact.genSnapshot:
650650
assert(mode != BuildMode.debug, 'Artifact $artifact only available in non-debug mode.');
651-
final String hostPlatform = getNameForHostPlatform(getCurrentHostPlatform());
652-
return _fileSystem.path.join(engineDir, hostPlatform, _artifactToFileName(artifact, _platform));
651+
652+
// TODO(cbracken): Build Android gen_snapshot as Arm64 binary to run
653+
// natively on Apple Silicon. See:
654+
// https://github.com/flutter/flutter/issues/152281
655+
HostPlatform hostPlatform = getCurrentHostPlatform();
656+
if (hostPlatform == HostPlatform.darwin_arm64) {
657+
hostPlatform = HostPlatform.darwin_x64;
658+
}
659+
660+
final String hostPlatformName = getNameForHostPlatform(hostPlatform);
661+
return _fileSystem.path.join(engineDir, hostPlatformName, _artifactToFileName(artifact, _platform));
653662
case Artifact.engineDartSdkPath:
654663
case Artifact.engineDartBinary:
655664
case Artifact.engineDartAotRuntime:

packages/flutter_tools/lib/src/build_info.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,9 +788,23 @@ AndroidArch getAndroidArchForName(String platform) {
788788
};
789789
}
790790

791+
DarwinArch getCurrentDarwinArch() {
792+
return switch (globals.os.hostPlatform) {
793+
HostPlatform.darwin_arm64 => DarwinArch.arm64,
794+
HostPlatform.darwin_x64 => DarwinArch.x86_64,
795+
final HostPlatform unsupported => throw Exception(
796+
'Unsupported Darwin host platform "$unsupported"',
797+
),
798+
};
799+
}
800+
791801
HostPlatform getCurrentHostPlatform() {
792802
if (globals.platform.isMacOS) {
793-
return HostPlatform.darwin_x64;
803+
return switch (getCurrentDarwinArch()) {
804+
DarwinArch.arm64 => HostPlatform.darwin_arm64,
805+
DarwinArch.x86_64 => HostPlatform.darwin_x64,
806+
DarwinArch.armv7 => throw Exception('Unsupported macOS arch "amv7"'),
807+
};
794808
}
795809
if (globals.platform.isLinux) {
796810
// support x64 and arm64 architecture.

packages/flutter_tools/lib/src/ios/mac.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,15 @@ Future<XcodeBuildResult> buildXcodeProject({
351351
}
352352

353353
if (activeArch != null) {
354-
final String activeArchName = activeArch.name;
355-
buildCommands.add('ONLY_ACTIVE_ARCH=YES');
356354
// Setting ARCHS to $activeArchName will break the build if a watchOS companion app exists,
357355
// as it cannot be build for the architecture of the Flutter app.
358356
if (!hasWatchCompanion) {
359-
buildCommands.add('ARCHS=$activeArchName');
357+
// ONLY_ACTIVE_ARCH specifies whether the product includes only code for
358+
// the native architecture.
359+
final bool onlyActiveArch = activeArch == getCurrentDarwinArch();
360+
361+
buildCommands.add('ONLY_ACTIVE_ARCH=${onlyActiveArch? 'YES' : 'NO'}');
362+
buildCommands.add('ARCHS=${activeArch.name}');
360363
}
361364
}
362365

packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:file_testing/file_testing.dart';
1010
import 'package:flutter_tools/src/artifacts.dart';
1111
import 'package:flutter_tools/src/base/file_system.dart';
1212
import 'package:flutter_tools/src/base/logger.dart';
13+
import 'package:flutter_tools/src/base/os.dart';
1314
import 'package:flutter_tools/src/base/platform.dart';
1415
import 'package:flutter_tools/src/base/version.dart';
1516
import 'package:flutter_tools/src/build_info.dart';
@@ -79,6 +80,10 @@ final FakePlatform macPlatform = FakePlatform(
7980
environment: <String, String>{},
8081
);
8182

83+
final FakeOperatingSystemUtils os = FakeOperatingSystemUtils(
84+
hostPlatform: HostPlatform.darwin_arm64,
85+
);
86+
8287
void main() {
8388
late Artifacts artifacts;
8489
late String iosDeployPath;
@@ -153,6 +158,7 @@ void main() {
153158
ProcessManager: () => processManager,
154159
FileSystem: () => fileSystem,
155160
Logger: () => logger,
161+
OperatingSystemUtils: () => os,
156162
Platform: () => macPlatform,
157163
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(buildSettings: const <String, String>{
158164
'WRAPPER_NAME': 'My Super Awesome App.app',
@@ -243,6 +249,92 @@ void main() {
243249
ProcessManager: () => processManager,
244250
FileSystem: () => fileSystem,
245251
Logger: () => logger,
252+
OperatingSystemUtils: () => os,
253+
Platform: () => macPlatform,
254+
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
255+
Xcode: () => xcode,
256+
});
257+
258+
testUsingContext('ONLY_ACTIVE_ARCH is NO if different host and target architectures', () async {
259+
// Host architecture is x64, target architecture is arm64.
260+
final IOSDevice iosDevice = setUpIOSDevice(
261+
fileSystem: fileSystem,
262+
processManager: processManager,
263+
logger: logger,
264+
artifacts: artifacts,
265+
);
266+
setUpIOSProject(fileSystem);
267+
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
268+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
269+
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
270+
271+
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
272+
processManager.addCommand(const FakeCommand(command: <String>[
273+
'xcrun',
274+
'xcodebuild',
275+
'-configuration',
276+
'Release',
277+
'-quiet',
278+
'-workspace',
279+
'Runner.xcworkspace',
280+
'-scheme',
281+
'Runner',
282+
'BUILD_DIR=/build/ios',
283+
'-sdk',
284+
'iphoneos',
285+
'-destination',
286+
'id=123',
287+
'ONLY_ACTIVE_ARCH=NO',
288+
'ARCHS=arm64',
289+
'-resultBundlePath',
290+
'/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle',
291+
'-resultBundleVersion',
292+
'3',
293+
'FLUTTER_SUPPRESS_ANALYTICS=true',
294+
'COMPILER_INDEX_STORE_ENABLE=NO',
295+
]));
296+
processManager.addCommand(const FakeCommand(command: <String>[
297+
'rsync',
298+
'-8',
299+
'-av',
300+
'--delete',
301+
'build/ios/Release-iphoneos/My Super Awesome App.app',
302+
'build/ios/iphoneos',
303+
]));
304+
processManager.addCommand(FakeCommand(
305+
command: <String>[
306+
iosDeployPath,
307+
'--id',
308+
'123',
309+
'--bundle',
310+
'build/ios/iphoneos/My Super Awesome App.app',
311+
'--app_deltas',
312+
'build/ios/app-delta',
313+
'--no-wifi',
314+
'--justlaunch',
315+
'--args',
316+
const <String>[
317+
'--enable-dart-profiling',
318+
].join(' '),
319+
])
320+
);
321+
322+
final LaunchResult launchResult = await iosDevice.startApp(
323+
buildableIOSApp,
324+
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
325+
platformArgs: <String, Object>{},
326+
);
327+
328+
expect(fileSystem.directory('build/ios/iphoneos'), exists);
329+
expect(launchResult.started, true);
330+
expect(processManager, hasNoRemainingExpectations);
331+
}, overrides: <Type, Generator>{
332+
ProcessManager: () => processManager,
333+
FileSystem: () => fileSystem,
334+
Logger: () => logger,
335+
OperatingSystemUtils: () => FakeOperatingSystemUtils(
336+
hostPlatform: HostPlatform.darwin_x64,
337+
),
246338
Platform: () => macPlatform,
247339
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
248340
Xcode: () => xcode,
@@ -362,6 +454,7 @@ void main() {
362454
ProcessManager: () => FakeProcessManager.any(),
363455
FileSystem: () => fileSystem,
364456
Logger: () => logger,
457+
OperatingSystemUtils: () => os,
365458
Platform: () => macPlatform,
366459
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
367460
Xcode: () => xcode,
@@ -396,6 +489,7 @@ void main() {
396489
ProcessManager: () => FakeProcessManager.any(),
397490
FileSystem: () => fileSystem,
398491
Logger: () => logger,
492+
OperatingSystemUtils: () => os,
399493
Platform: () => macPlatform,
400494
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
401495
Xcode: () => xcode,
@@ -430,6 +524,7 @@ void main() {
430524
ProcessManager: () => FakeProcessManager.any(),
431525
FileSystem: () => fileSystem,
432526
Logger: () => logger,
527+
OperatingSystemUtils: () => os,
433528
Platform: () => macPlatform,
434529
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
435530
Xcode: () => xcode,
@@ -465,6 +560,7 @@ void main() {
465560
ProcessManager: () => FakeProcessManager.any(),
466561
FileSystem: () => fileSystem,
467562
Logger: () => logger,
563+
OperatingSystemUtils: () => os,
468564
Platform: () => macPlatform,
469565
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
470566
Xcode: () => xcode,
@@ -531,6 +627,7 @@ void main() {
531627
ProcessManager: () => FakeProcessManager.any(),
532628
FileSystem: () => fileSystem,
533629
Logger: () => logger,
630+
OperatingSystemUtils: () => os,
534631
Platform: () => macPlatform,
535632
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
536633
Xcode: () => xcode,
@@ -606,6 +703,7 @@ void main() {
606703
ProcessManager: () => FakeProcessManager.any(),
607704
FileSystem: () => fileSystem,
608705
Logger: () => logger,
706+
OperatingSystemUtils: () => os,
609707
Platform: () => macPlatform,
610708
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
611709
Xcode: () => xcode,
@@ -686,6 +784,7 @@ void main() {
686784
ProcessManager: () => FakeProcessManager.any(),
687785
FileSystem: () => fileSystem,
688786
Logger: () => logger,
787+
OperatingSystemUtils: () => os,
689788
Platform: () => macPlatform,
690789
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
691790
Xcode: () => xcode,
@@ -764,6 +863,7 @@ void main() {
764863
ProcessManager: () => FakeProcessManager.any(),
765864
FileSystem: () => fileSystem,
766865
Logger: () => logger,
866+
OperatingSystemUtils: () => os,
767867
Platform: () => macPlatform,
768868
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
769869
Xcode: () => xcode,
@@ -839,6 +939,7 @@ IOSDevice setUpIOSDevice({
839939
bool isCoreDevice = false,
840940
IOSCoreDeviceControl? coreDeviceControl,
841941
FakeXcodeDebug? xcodeDebug,
942+
DarwinArch cpuArchitecture = DarwinArch.arm64,
842943
}) {
843944
artifacts ??= Artifacts.test();
844945
final Cache cache = Cache.test(
@@ -872,7 +973,7 @@ IOSDevice setUpIOSDevice({
872973
),
873974
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
874975
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
875-
cpuArchitecture: DarwinArch.arm64,
976+
cpuArchitecture: cpuArchitecture,
876977
connectionInterface: DeviceConnectionInterface.attached,
877978
isConnected: true,
878979
isPaired: true,

packages/flutter_tools/test/general.shard/xcode_backend_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,57 @@ void main() {
450450
expect(context.stderr, isEmpty);
451451
});
452452

453+
test('does not assumes ARCHS if ARCHS and NATIVE_ARCH are different', () {
454+
final Directory buildDir = fileSystem.directory('/path/to/builds')
455+
..createSync(recursive: true);
456+
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
457+
..createSync(recursive: true);
458+
final File pipe = fileSystem.file('/tmp/pipe')
459+
..createSync(recursive: true);
460+
const String buildMode = 'Debug';
461+
final TestContext context = TestContext(
462+
<String>['prepare'],
463+
<String, String>{
464+
'BUILT_PRODUCTS_DIR': buildDir.path,
465+
'CONFIGURATION': buildMode,
466+
'FLUTTER_ROOT': flutterRoot.path,
467+
'INFOPLIST_PATH': 'Info.plist',
468+
'ARCHS': 'arm64',
469+
'ONLY_ACTIVE_ARCH': 'YES',
470+
'NATIVE_ARCH': 'x86_64',
471+
},
472+
commands: <FakeCommand>[
473+
FakeCommand(
474+
command: <String>[
475+
'${flutterRoot.path}/bin/flutter',
476+
'assemble',
477+
'--no-version-check',
478+
'--output=${buildDir.path}/',
479+
'-dTargetPlatform=ios',
480+
'-dTargetFile=lib/main.dart',
481+
'-dBuildMode=${buildMode.toLowerCase()}',
482+
'-dIosArchs=arm64',
483+
'-dSdkRoot=',
484+
'-dSplitDebugInfo=',
485+
'-dTreeShakeIcons=',
486+
'-dTrackWidgetCreation=',
487+
'-dDartObfuscation=',
488+
'-dAction=',
489+
'-dFrontendServerStarterPath=',
490+
'--ExtraGenSnapshotOptions=',
491+
'--DartDefines=',
492+
'--ExtraFrontEndOptions=',
493+
'-dPreBuildAction=PrepareFramework',
494+
'debug_unpack_ios',
495+
],
496+
),
497+
],
498+
fileSystem: fileSystem,
499+
scriptOutputStreamFile: pipe,
500+
)..run();
501+
expect(context.stderr, isEmpty);
502+
});
503+
453504
test('does not assumes ARCHS if ONLY_ACTIVE_ARCH is not YES', () {
454505
final Directory buildDir = fileSystem.directory('/path/to/builds')
455506
..createSync(recursive: true);

0 commit comments

Comments
 (0)