Unverified Commit 849676fc authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Support multi-arch iOS binaries (#17312)

This change adds support for armv7, arm64, and universal iOS apps.

This change eliminates iOS target architecture hardcoding (previously
arm64 only) and uses the target architecture(s) specified in Xcode's
ARCHS setting ('Architectures' in Xcode Build Settings).

For universal binaries, set ARCHS to its default value, $(ARCHS_STANDARD).

Note that after changing the architecture in Xcode, developers should
run 'pod install' from the ios subdirectory of their project. A separate
change (that will land before this one) will add support for
automatically detecting project file and Podfile changes and re-running
pod install if necessary.

This change also adds an --ios-arch option to flutter build aot. In iOS
AOT builds (in profile and release mode), this dictates which
architectures are built into App.framework. This flag should generally
be unnecessary to set manually since flutter build aot is typically only
invoked internally by flutter itself.
parent 5834f417
...@@ -111,6 +111,8 @@ BuildApp() { ...@@ -111,6 +111,8 @@ BuildApp() {
if [[ "${build_mode}" != "debug" ]]; then if [[ "${build_mode}" != "debug" ]]; then
StreamOutput " ├─Building Dart code..." StreamOutput " ├─Building Dart code..."
# Transform ARCHS to comma-separated list of target architectures.
local archs="${ARCHS// /,}"
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \ RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \ ${verbose_flag} \
build aot \ build aot \
...@@ -118,6 +120,7 @@ BuildApp() { ...@@ -118,6 +120,7 @@ BuildApp() {
--target-platform=ios \ --target-platform=ios \
--target="${target_path}" \ --target="${target_path}" \
--${build_mode} \ --${build_mode} \
--ios-arch="${archs}" \
${local_engine_flag} \ ${local_engine_flag} \
${preview_dart_2_flag} ${preview_dart_2_flag}
......
...@@ -40,6 +40,7 @@ class GenSnapshot { ...@@ -40,6 +40,7 @@ class GenSnapshot {
@required SnapshotType snapshotType, @required SnapshotType snapshotType,
@required String packagesPath, @required String packagesPath,
@required String depfilePath, @required String depfilePath,
IOSArch iosArch,
Iterable<String> additionalArgs: const <String>[], Iterable<String> additionalArgs: const <String>[],
}) { }) {
final List<String> args = <String>[ final List<String> args = <String>[
...@@ -56,8 +57,8 @@ class GenSnapshot { ...@@ -56,8 +57,8 @@ class GenSnapshot {
// code. /usr/bin/arch can be used to run binaries with the specified // code. /usr/bin/arch can be used to run binaries with the specified
// architecture. // architecture.
if (snapshotType.platform == TargetPlatform.ios) { if (snapshotType.platform == TargetPlatform.ios) {
// TODO(cbracken): for the moment, always generate only arm64 code. final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64';
return runCommandAndStreamOutput(<String>['/usr/bin/arch', '-x86_64', snapshotterPath]..addAll(args)); return runCommandAndStreamOutput(<String>['/usr/bin/arch', hostArch, snapshotterPath]..addAll(args));
} }
return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args)); return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
} }
...@@ -137,12 +138,15 @@ class AOTSnapshotter { ...@@ -137,12 +138,15 @@ class AOTSnapshotter {
@required String outputPath, @required String outputPath,
@required bool previewDart2, @required bool previewDart2,
@required bool preferSharedLibrary, @required bool preferSharedLibrary,
IOSArch iosArch,
List<String> extraGenSnapshotOptions: const <String>[], List<String> extraGenSnapshotOptions: const <String>[],
}) async { }) async {
if (!_isValidAotPlatform(platform, buildMode)) { if (!_isValidAotPlatform(platform, buildMode)) {
printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.'); printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
return -1; return -1;
} }
// TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
assert(platform != TargetPlatform.ios || iosArch != null);
final bool compileToSharedLibrary = preferSharedLibrary && androidSdk.ndkCompiler != null; final bool compileToSharedLibrary = preferSharedLibrary && androidSdk.ndkCompiler != null;
if (preferSharedLibrary && !compileToSharedLibrary) { if (preferSharedLibrary && !compileToSharedLibrary) {
...@@ -215,7 +219,7 @@ class AOTSnapshotter { ...@@ -215,7 +219,7 @@ class AOTSnapshotter {
]); ]);
} }
if (platform == TargetPlatform.android_arm) { if (platform == TargetPlatform.android_arm || iosArch == IOSArch.armv7) {
// Not supported by the Pixel in 32-bit mode. // Not supported by the Pixel in 32-bit mode.
genSnapshotArgs.add('--no-use-integer-division'); genSnapshotArgs.add('--no-use-integer-division');
} }
...@@ -253,6 +257,7 @@ class AOTSnapshotter { ...@@ -253,6 +257,7 @@ class AOTSnapshotter {
packagesPath: packageMap.packagesPath, packagesPath: packageMap.packagesPath,
depfilePath: depfilePath, depfilePath: depfilePath,
additionalArgs: genSnapshotArgs, additionalArgs: genSnapshotArgs,
iosArch: iosArch,
); );
if (genSnapshotExitCode != 0) { if (genSnapshotExitCode != 0) {
printError('Dart snapshot generator failed with exit code $genSnapshotExitCode'); printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
...@@ -266,7 +271,7 @@ class AOTSnapshotter { ...@@ -266,7 +271,7 @@ class AOTSnapshotter {
// On iOS, we use Xcode to compile the snapshot into a dynamic library that the // On iOS, we use Xcode to compile the snapshot into a dynamic library that the
// end-developer can link into their app. // end-developer can link into their app.
if (platform == TargetPlatform.ios) { if (platform == TargetPlatform.ios) {
final RunResult result = await _buildIosFramework(assemblyPath: assembly, outputPath: outputDir.path); final RunResult result = await _buildIosFramework(iosArch: iosArch, assemblyPath: assembly, outputPath: outputDir.path);
if (result.exitCode != 0) if (result.exitCode != 0)
return result.exitCode; return result.exitCode;
} else if (compileToSharedLibrary) { } else if (compileToSharedLibrary) {
...@@ -283,11 +288,13 @@ class AOTSnapshotter { ...@@ -283,11 +288,13 @@ class AOTSnapshotter {
/// Builds an iOS framework at [outputPath]/App.framework from the assembly /// Builds an iOS framework at [outputPath]/App.framework from the assembly
/// source at [assemblyPath]. /// source at [assemblyPath].
Future<RunResult> _buildIosFramework({ Future<RunResult> _buildIosFramework({
@required IOSArch iosArch,
@required String assemblyPath, @required String assemblyPath,
@required String outputPath, @required String outputPath,
}) async { }) async {
printStatus('Building App.framework...'); final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64';
const List<String> commonBuildOptions = const <String>['-arch', 'arm64', '-miphoneos-version-min=8.0']; printStatus('Building App.framework for $targetArch...');
final List<String> commonBuildOptions = <String>['-arch', targetArch, '-miphoneos-version-min=8.0'];
final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o'); final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
final RunResult compileResult = await xcode.cc(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO])); final RunResult compileResult = await xcode.cc(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO]));
......
...@@ -160,6 +160,41 @@ enum TargetPlatform { ...@@ -160,6 +160,41 @@ enum TargetPlatform {
tester, tester,
} }
/// iOS target device architecture.
//
// TODO(cbracken): split TargetPlatform.ios into ios_armv7, ios_arm64.
enum IOSArch {
armv7,
arm64,
}
/// The default set of iOS device architectures to build for.
const List<IOSArch> defaultIOSArchs = const <IOSArch>[
IOSArch.arm64,
];
String getNameForIOSArch(IOSArch arch) {
switch (arch) {
case IOSArch.armv7:
return 'armv7';
case IOSArch.arm64:
return 'arm64';
}
assert(false);
return null;
}
IOSArch getIOSArchForName(String arch) {
switch (arch) {
case 'armv7':
return IOSArch.armv7;
case 'arm64':
return IOSArch.arm64;
}
assert(false);
return null;
}
String getNameForTargetPlatform(TargetPlatform platform) { String getNameForTargetPlatform(TargetPlatform platform) {
switch (platform) { switch (platform) {
case TargetPlatform.android_arm: case TargetPlatform.android_arm:
......
...@@ -8,6 +8,7 @@ import '../base/build.dart'; ...@@ -8,6 +8,7 @@ import '../base/build.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
...@@ -32,6 +33,12 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -32,6 +33,12 @@ class BuildAotCommand extends BuildSubCommand {
hide: !verboseHelp, hide: !verboseHelp,
help: 'Preview Dart 2.0 functionality.', help: 'Preview Dart 2.0 functionality.',
) )
..addMultiOption('ios-arch',
splitCommas: true,
defaultsTo: defaultIOSArchs.map(getNameForIOSArch),
allowed: IOSArch.values.map(getNameForIOSArch),
help: 'iOS architectures to build',
)
..addMultiOption(FlutterOptions.kExtraFrontEndOptions, ..addMultiOption(FlutterOptions.kExtraFrontEndOptions,
splitCommas: true, splitCommas: true,
hide: true, hide: true,
...@@ -90,18 +97,57 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -90,18 +97,57 @@ class BuildAotCommand extends BuildSubCommand {
} }
// Build AOT snapshot. // Build AOT snapshot.
final int snapshotExitCode = await snapshotter.build( if (platform == TargetPlatform.ios) {
platform: platform, // Determine which iOS architectures to build for.
buildMode: buildMode, final Iterable<IOSArch> buildArchs = argResults['ios-arch'].map(getIOSArchForName);
mainPath: mainPath, final Map<IOSArch, String> iosBuilds = <IOSArch, String>{};
packagesPath: PackageMap.globalPackagesPath, for (IOSArch arch in buildArchs)
outputPath: outputPath, iosBuilds[arch] = fs.path.join(outputPath, getNameForIOSArch(arch));
previewDart2: previewDart2,
preferSharedLibrary: argResults['prefer-shared-library'], // Generate AOT snapshot and compile to arch-specific App.framework.
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], final Map<IOSArch, Future<int>> exitCodes = <IOSArch, Future<int>>{};
); iosBuilds.forEach((IOSArch iosArch, String outputPath) {
if (snapshotExitCode != 0) { exitCodes[iosArch] = snapshotter.build(
printError('Snapshotting exited with non-zero exit code: $snapshotExitCode'); platform: platform,
iosArch: iosArch,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
previewDart2: previewDart2,
preferSharedLibrary: false,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
).then((int buildExitCode) {
if (buildExitCode != 0)
printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
return buildExitCode;
});
});
// Merge arch-specific App.frameworks into a multi-arch App.framework.
if ((await Future.wait(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) {
final Iterable<String> dylibs = iosBuilds.values.map((String outputDir) => fs.path.join(outputDir, 'App.framework', 'App'));
fs.directory(fs.path.join(outputPath, 'App.framework'))..createSync();
await runCheckedAsync(<String>['lipo']
..addAll(dylibs)
..addAll(<String>['-create', '-output', fs.path.join(outputPath, 'App.framework', 'App')]),
);
}
} else {
// Android AOT snapshot.
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
previewDart2: previewDart2,
preferSharedLibrary: argResults['prefer-shared-library'],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
);
if (snapshotExitCode != 0) {
printError('Snapshotting exited with non-zero exit code: $snapshotExitCode');
}
} }
} on String catch (error) { } on String catch (error) {
// Catch the String exceptions thrown from the `runCheckedSync` methods below. // Catch the String exceptions thrown from the `runCheckedSync` methods below.
......
...@@ -325,7 +325,6 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -325,7 +325,6 @@ Future<XcodeBuildResult> buildXcodeProject({
'xcrun', 'xcrun',
'xcodebuild', 'xcodebuild',
'-configuration', configuration, '-configuration', configuration,
'ONLY_ACTIVE_ARCH=YES',
]; ];
if (logger.isVerbose) { if (logger.isVerbose) {
...@@ -358,7 +357,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -358,7 +357,7 @@ Future<XcodeBuildResult> buildXcodeProject({
} }
if (buildForDevice) { if (buildForDevice) {
buildCommands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']); buildCommands.addAll(<String>['-sdk', 'iphoneos']);
} else { } else {
buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
} }
......
...@@ -52,6 +52,7 @@ class _FakeGenSnapshot implements GenSnapshot { ...@@ -52,6 +52,7 @@ class _FakeGenSnapshot implements GenSnapshot {
SnapshotType snapshotType, SnapshotType snapshotType,
String packagesPath, String packagesPath,
String depfilePath, String depfilePath,
IOSArch iosArch,
Iterable<String> additionalArgs, Iterable<String> additionalArgs,
}) async { }) async {
_callCount += 1; _callCount += 1;
...@@ -361,6 +362,7 @@ void main() { ...@@ -361,6 +362,7 @@ void main() {
outputPath: outputPath, outputPath: outputPath,
preferSharedLibrary: false, preferSharedLibrary: false,
previewDart2: true, previewDart2: true,
iosArch: IOSArch.arm64,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
...@@ -407,6 +409,7 @@ void main() { ...@@ -407,6 +409,7 @@ void main() {
outputPath: outputPath, outputPath: outputPath,
preferSharedLibrary: false, preferSharedLibrary: false,
previewDart2: true, previewDart2: true,
iosArch: IOSArch.arm64,
); );
expect(genSnapshotExitCode, 0); expect(genSnapshotExitCode, 0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment