Skip to content

Commit

Permalink
Support multi-arch iOS binaries (#17312)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cbracken committed May 7, 2018
1 parent 5834f41 commit 849676f
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 20 deletions.
3 changes: 3 additions & 0 deletions packages/flutter_tools/bin/xcode_backend.sh
Expand Up @@ -111,13 +111,16 @@ BuildApp() {

if [[ "${build_mode}" != "debug" ]]; then
StreamOutput " ├─Building Dart code..."
# Transform ARCHS to comma-separated list of target architectures.
local archs="${ARCHS// /,}"
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \
build aot \
--output-dir="${build_dir}/aot" \
--target-platform=ios \
--target="${target_path}" \
--${build_mode} \
--ios-arch="${archs}" \
${local_engine_flag} \
${preview_dart_2_flag}

Expand Down
19 changes: 13 additions & 6 deletions packages/flutter_tools/lib/src/base/build.dart
Expand Up @@ -40,6 +40,7 @@ class GenSnapshot {
@required SnapshotType snapshotType,
@required String packagesPath,
@required String depfilePath,
IOSArch iosArch,
Iterable<String> additionalArgs: const <String>[],
}) {
final List<String> args = <String>[
Expand All @@ -56,8 +57,8 @@ class GenSnapshot {
// code. /usr/bin/arch can be used to run binaries with the specified
// architecture.
if (snapshotType.platform == TargetPlatform.ios) {
// TODO(cbracken): for the moment, always generate only arm64 code.
return runCommandAndStreamOutput(<String>['/usr/bin/arch', '-x86_64', snapshotterPath]..addAll(args));
final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64';
return runCommandAndStreamOutput(<String>['/usr/bin/arch', hostArch, snapshotterPath]..addAll(args));
}
return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
}
Expand Down Expand Up @@ -137,12 +138,15 @@ class AOTSnapshotter {
@required String outputPath,
@required bool previewDart2,
@required bool preferSharedLibrary,
IOSArch iosArch,
List<String> extraGenSnapshotOptions: const <String>[],
}) async {
if (!_isValidAotPlatform(platform, buildMode)) {
printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
return -1;
}
// TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
assert(platform != TargetPlatform.ios || iosArch != null);

final bool compileToSharedLibrary = preferSharedLibrary && androidSdk.ndkCompiler != null;
if (preferSharedLibrary && !compileToSharedLibrary) {
Expand Down Expand Up @@ -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.
genSnapshotArgs.add('--no-use-integer-division');
}
Expand Down Expand Up @@ -253,6 +257,7 @@ class AOTSnapshotter {
packagesPath: packageMap.packagesPath,
depfilePath: depfilePath,
additionalArgs: genSnapshotArgs,
iosArch: iosArch,
);
if (genSnapshotExitCode != 0) {
printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
Expand All @@ -266,7 +271,7 @@ class AOTSnapshotter {
// On iOS, we use Xcode to compile the snapshot into a dynamic library that the
// end-developer can link into their app.
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)
return result.exitCode;
} else if (compileToSharedLibrary) {
Expand All @@ -283,11 +288,13 @@ class AOTSnapshotter {
/// Builds an iOS framework at [outputPath]/App.framework from the assembly
/// source at [assemblyPath].
Future<RunResult> _buildIosFramework({
@required IOSArch iosArch,
@required String assemblyPath,
@required String outputPath,
}) async {
printStatus('Building App.framework...');
const List<String> commonBuildOptions = const <String>['-arch', 'arm64', '-miphoneos-version-min=8.0'];
final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64';
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 RunResult compileResult = await xcode.cc(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO]));
Expand Down
35 changes: 35 additions & 0 deletions packages/flutter_tools/lib/src/build_info.dart
Expand Up @@ -160,6 +160,41 @@ enum TargetPlatform {
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) {
switch (platform) {
case TargetPlatform.android_arm:
Expand Down
70 changes: 58 additions & 12 deletions packages/flutter_tools/lib/src/commands/build_aot.dart
Expand Up @@ -8,6 +8,7 @@ import '../base/build.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../dart/package_map.dart';
import '../globals.dart';
Expand All @@ -32,6 +33,12 @@ class BuildAotCommand extends BuildSubCommand {
hide: !verboseHelp,
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,
splitCommas: true,
hide: true,
Expand Down Expand Up @@ -90,18 +97,57 @@ class BuildAotCommand extends BuildSubCommand {
}

// Build 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');
if (platform == TargetPlatform.ios) {
// Determine which iOS architectures to build for.
final Iterable<IOSArch> buildArchs = argResults['ios-arch'].map(getIOSArchForName);
final Map<IOSArch, String> iosBuilds = <IOSArch, String>{};
for (IOSArch arch in buildArchs)
iosBuilds[arch] = fs.path.join(outputPath, getNameForIOSArch(arch));

// Generate AOT snapshot and compile to arch-specific App.framework.
final Map<IOSArch, Future<int>> exitCodes = <IOSArch, Future<int>>{};
iosBuilds.forEach((IOSArch iosArch, String outputPath) {
exitCodes[iosArch] = snapshotter.build(
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) {
// Catch the String exceptions thrown from the `runCheckedSync` methods below.
Expand Down
3 changes: 1 addition & 2 deletions packages/flutter_tools/lib/src/ios/mac.dart
Expand Up @@ -325,7 +325,6 @@ Future<XcodeBuildResult> buildXcodeProject({
'xcrun',
'xcodebuild',
'-configuration', configuration,
'ONLY_ACTIVE_ARCH=YES',
];

if (logger.isVerbose) {
Expand Down Expand Up @@ -358,7 +357,7 @@ Future<XcodeBuildResult> buildXcodeProject({
}

if (buildForDevice) {
buildCommands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
buildCommands.addAll(<String>['-sdk', 'iphoneos']);
} else {
buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/flutter_tools/test/base/build_test.dart
Expand Up @@ -52,6 +52,7 @@ class _FakeGenSnapshot implements GenSnapshot {
SnapshotType snapshotType,
String packagesPath,
String depfilePath,
IOSArch iosArch,
Iterable<String> additionalArgs,
}) async {
_callCount += 1;
Expand Down Expand Up @@ -361,6 +362,7 @@ void main() {
outputPath: outputPath,
preferSharedLibrary: false,
previewDart2: true,
iosArch: IOSArch.arm64,
);

expect(genSnapshotExitCode, 0);
Expand Down Expand Up @@ -407,6 +409,7 @@ void main() {
outputPath: outputPath,
preferSharedLibrary: false,
previewDart2: true,
iosArch: IOSArch.arm64,
);

expect(genSnapshotExitCode, 0);
Expand Down

0 comments on commit 849676f

Please sign in to comment.