blob: 644d48aff066fde372b39a3cedd4a79888f93b6e [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
import '../../globals.dart' as globals show xcode;
import '../../macos/xcode.dart';
import '../../project.dart';
import '../../reporting/reporting.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
import 'assets.dart';
import 'common.dart';
import 'icon_tree_shaker.dart';
import 'shader_compiler.dart';
/// Supports compiling a dart kernel file to an assembly file.
///
/// If more than one iOS arch is provided, then this rule will
/// produce a universal binary.
abstract class AotAssemblyBase extends Target {
const AotAssemblyBase();
@override
String get analyticsName => 'ios_aot';
@override
Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(
fileSystem: environment.fileSystem,
logger: environment.logger,
xcode: globals.xcode!,
artifacts: environment.artifacts,
processManager: environment.processManager,
);
final String buildOutputPath = environment.buildDir.path;
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, 'aot_assembly');
}
final String? environmentTargetPlatform = environment.defines[kTargetPlatform];
if (environmentTargetPlatform== null) {
throw MissingDefineException(kTargetPlatform, 'aot_assembly');
}
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, 'aot_assembly');
}
final List<String> extraGenSnapshotOptions = decodeCommaSeparated(environment.defines, kExtraGenSnapshotOptions);
final BuildMode buildMode = getBuildModeForName(environmentBuildMode);
final TargetPlatform targetPlatform = getTargetPlatformForName(environmentTargetPlatform);
final String? splitDebugInfo = environment.defines[kSplitDebugInfo];
final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true';
final List<DarwinArch> darwinArchs = environment.defines[kIosArchs]
?.split(' ')
.map(getIOSArchForName)
.toList()
?? <DarwinArch>[DarwinArch.arm64];
if (targetPlatform != TargetPlatform.ios) {
throw Exception('aot_assembly is only supported for iOS applications.');
}
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem);
if (environmentType == EnvironmentType.simulator) {
throw Exception(
'release/profile builds are only supported for physical devices. '
'attempted to build for simulator.'
);
}
final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory];
// If we're building multiple iOS archs the binaries need to be lipo'd
// together.
final List<Future<int>> pending = <Future<int>>[];
for (final DarwinArch darwinArch in darwinArchs) {
final List<String> archExtraGenSnapshotOptions = List<String>.of(extraGenSnapshotOptions);
if (codeSizeDirectory != null) {
final File codeSizeFile = environment.fileSystem
.directory(codeSizeDirectory)
.childFile('snapshot.${getNameForDarwinArch(darwinArch)}.json');
final File precompilerTraceFile = environment.fileSystem
.directory(codeSizeDirectory)
.childFile('trace.${getNameForDarwinArch(darwinArch)}.json');
archExtraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}');
archExtraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}');
}
pending.add(snapshotter.build(
platform: targetPlatform,
buildMode: buildMode,
mainPath: environment.buildDir.childFile('app.dill').path,
outputPath: environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(darwinArch)),
darwinArch: darwinArch,
sdkRoot: sdkRoot,
quiet: true,
splitDebugInfo: splitDebugInfo,
dartObfuscation: dartObfuscation,
extraGenSnapshotOptions: archExtraGenSnapshotOptions,
));
}
final List<int> results = await Future.wait(pending);
if (results.any((int result) => result != 0)) {
throw Exception('AOT snapshotter exited with code ${results.join()}');
}
// Combine the app lib into a fat framework.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework/App',
inputDir: buildOutputPath,
);
// And combine the dSYM for each architecture too, if it was created.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App',
inputDir: buildOutputPath,
// Don't fail if the dSYM wasn't created (i.e. during a debug build).
skipMissingInputs: true,
);
}
}
/// Generate an assembly target from a dart kernel file in release mode.
class AotAssemblyRelease extends AotAssemblyBase {
const AotAssemblyRelease();
@override
String get name => 'aot_assembly_release';
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
Source.pattern('{BUILD_DIR}/app.dill'),
Source.hostArtifact(HostArtifact.engineDartBinary),
Source.artifact(Artifact.skyEnginePath),
// TODO(zanderso): cannot reference gen_snapshot with artifacts since
// it resolves to a file (ios/gen_snapshot) that never exists. This was
// split into gen_snapshot_arm64 and gen_snapshot_armv7.
// Source.artifact(Artifact.genSnapshot,
// platform: TargetPlatform.ios,
// mode: BuildMode.release,
// ),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/App.framework/App'),
];
@override
List<Target> get dependencies => const <Target>[
ReleaseUnpackIOS(),
KernelSnapshot(),
];
}
/// Generate an assembly target from a dart kernel file in profile mode.
class AotAssemblyProfile extends AotAssemblyBase {
const AotAssemblyProfile();
@override
String get name => 'aot_assembly_profile';
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
Source.pattern('{BUILD_DIR}/app.dill'),
Source.hostArtifact(HostArtifact.engineDartBinary),
Source.artifact(Artifact.skyEnginePath),
// TODO(zanderso): cannot reference gen_snapshot with artifacts since
// it resolves to a file (ios/gen_snapshot) that never exists. This was
// split into gen_snapshot_arm64 and gen_snapshot_armv7.
// Source.artifact(Artifact.genSnapshot,
// platform: TargetPlatform.ios,
// mode: BuildMode.profile,
// ),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/App.framework/App'),
];
@override
List<Target> get dependencies => const <Target>[
ProfileUnpackIOS(),
KernelSnapshot(),
];
}
/// Create a trivial App.framework file for debug iOS builds.
class DebugUniversalFramework extends Target {
const DebugUniversalFramework();
@override
String get name => 'debug_universal_framework';
@override
List<Target> get dependencies => const <Target>[
DebugUnpackIOS(),
KernelSnapshot(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/App.framework/App'),
];
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
// Generate a trivial App.framework.
final Set<String>? iosArchNames = environment.defines[kIosArchs]?.split(' ').toSet();
final File output = environment.buildDir
.childDirectory('App.framework')
.childFile('App');
environment.buildDir.createSync(recursive: true);
await _createStubAppFramework(
output,
environment,
iosArchNames,
sdkRoot,
);
}
}
/// Copy the iOS framework to the correct copy dir by invoking 'rsync'.
///
/// This class is abstract to share logic between the three concrete
/// implementations. The shelling out is done to avoid complications with
/// preserving special files (e.g., symbolic links) in the framework structure.
abstract class UnpackIOS extends Target {
const UnpackIOS();
@override
List<Source> get inputs => <Source>[
const Source.pattern(
'{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'),
Source.artifact(
Artifact.flutterXcframework,
platform: TargetPlatform.ios,
mode: buildMode,
),
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/Flutter.framework/Flutter'),
];
@override
List<Target> get dependencies => <Target>[];
@visibleForOverriding
BuildMode get buildMode;
@override
Future<void> build(Environment environment) async {
final String? sdkRoot = environment.defines[kSdkRoot];
if (sdkRoot == null) {
throw MissingDefineException(kSdkRoot, name);
}
final String? archs = environment.defines[kIosArchs];
if (archs == null) {
throw MissingDefineException(kIosArchs, name);
}
_copyFramework(environment, sdkRoot);
final File frameworkBinary = environment.outputDir.childDirectory('Flutter.framework').childFile('Flutter');
final String frameworkBinaryPath = frameworkBinary.path;
if (!frameworkBinary.existsSync()) {
throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin');
}
_thinFramework(environment, frameworkBinaryPath, archs);
if (buildMode == BuildMode.release) {
_bitcodeStripFramework(environment, frameworkBinaryPath);
}
_signFramework(environment, frameworkBinaryPath, buildMode);
}
void _copyFramework(Environment environment, String sdkRoot) {
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem);
final String basePath = environment.artifacts.getArtifactPath(
Artifact.flutterFramework,
platform: TargetPlatform.ios,
mode: buildMode,
environmentType: environmentType,
);
final ProcessResult result = environment.processManager.runSync(<String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
basePath,
environment.outputDir.path,
]);
if (result.exitCode != 0) {
throw Exception(
'Failed to copy framework (exit ${result.exitCode}:\n'
'${result.stdout}\n---\n${result.stderr}',
);
}
}
/// Destructively thin Flutter.framework to include only the specified architectures.
void _thinFramework(Environment environment, String frameworkBinaryPath, String archs) {
final List<String> archList = archs.split(' ').toList();
final ProcessResult infoResult = environment.processManager.runSync(<String>[
'lipo',
'-info',
frameworkBinaryPath,
]);
final String lipoInfo = infoResult.stdout as String;
final ProcessResult verifyResult = environment.processManager.runSync(<String>[
'lipo',
frameworkBinaryPath,
'-verify_arch',
...archList,
]);
if (verifyResult.exitCode != 0) {
throw Exception('Binary $frameworkBinaryPath does not contain $archs. Running lipo -info:\n$lipoInfo');
}
// Skip thinning for non-fat executables.
if (lipoInfo.startsWith('Non-fat file:')) {
environment.logger.printTrace('Skipping lipo for non-fat file $frameworkBinaryPath');
return;
}
// Thin in-place.
final ProcessResult extractResult = environment.processManager.runSync(<String>[
'lipo',
'-output',
frameworkBinaryPath,
for (final String arch in archList)
...<String>[
'-extract',
arch,
],
...<String>[frameworkBinaryPath],
]);
if (extractResult.exitCode != 0) {
throw Exception('Failed to extract $archs for $frameworkBinaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo');
}
}
/// Destructively strip bitcode from the framework. This can be removed
/// when the framework is no longer built with bitcode.
void _bitcodeStripFramework(Environment environment, String frameworkBinaryPath) {
final ProcessResult stripResult = environment.processManager.runSync(<String>[
'xcrun',
'bitcode_strip',
frameworkBinaryPath,
'-r', // Delete the bitcode segment.
'-o',
frameworkBinaryPath,
]);
if (stripResult.exitCode != 0) {
throw Exception('Failed to strip bitcode for $frameworkBinaryPath.\n${stripResult.stderr}');
}
}
}
/// Unpack the release prebuilt engine framework.
class ReleaseUnpackIOS extends UnpackIOS {
const ReleaseUnpackIOS();
@override
String get name => 'release_unpack_ios';
@override
BuildMode get buildMode => BuildMode.release;
}
/// Unpack the profile prebuilt engine framework.
class ProfileUnpackIOS extends UnpackIOS {
const ProfileUnpackIOS();
@override
String get name => 'profile_unpack_ios';
@override
BuildMode get buildMode => BuildMode.profile;
}
/// Unpack the debug prebuilt engine framework.
class DebugUnpackIOS extends UnpackIOS {
const DebugUnpackIOS();
@override
String get name => 'debug_unpack_ios';
@override
BuildMode get buildMode => BuildMode.debug;
}
/// The base class for all iOS bundle targets.
///
/// This is responsible for setting up the basic App.framework structure, including:
/// * Copying the app.dill/kernel_blob.bin from the build directory to assets (debug)
/// * Copying the precompiled isolate/vm data from the engine (debug)
/// * Copying the flutter assets to App.framework/flutter_assets
/// * Copying either the stub or real App assembly file to App.framework/App
abstract class IosAssetBundle extends Target {
const IosAssetBundle();
@override
List<Target> get dependencies => const <Target>[
KernelSnapshot(),
];
@override
List<Source> get inputs => const <Source>[
Source.pattern('{BUILD_DIR}/App.framework/App'),
Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
...IconTreeShaker.inputs,
...ShaderCompiler.inputs,
];
@override
List<Source> get outputs => const <Source>[
Source.pattern('{OUTPUT_DIR}/App.framework/App'),
Source.pattern('{OUTPUT_DIR}/App.framework/Info.plist'),
];
@override
List<String> get depfiles => <String>[
'flutter_assets.d',
];
@override
Future<void> build(Environment environment) async {
final String? environmentBuildMode = environment.defines[kBuildMode];
if (environmentBuildMode == null) {
throw MissingDefineException(kBuildMode, name);
}
final BuildMode buildMode = getBuildModeForName(environmentBuildMode);
final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
final String frameworkBinaryPath = frameworkDirectory.childFile('App').path;
final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
frameworkDirectory.createSync(recursive: true);
assetDirectory.createSync();
// Only copy the prebuilt runtimes and kernel blob in debug mode.
if (buildMode == BuildMode.debug) {
// Copy the App.framework to the output directory.
environment.buildDir
.childDirectory('App.framework')
.childFile('App')
.copySync(frameworkBinaryPath);
final String vmSnapshotData = environment.artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug);
final String isolateSnapshotData = environment.artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug);
environment.buildDir.childFile('app.dill')
.copySync(assetDirectory.childFile('kernel_blob.bin').path);
environment.fileSystem.file(vmSnapshotData)
.copySync(assetDirectory.childFile('vm_snapshot_data').path);
environment.fileSystem.file(isolateSnapshotData)
.copySync(assetDirectory.childFile('isolate_snapshot_data').path);
} else {
environment.buildDir.childDirectory('App.framework').childFile('App')
.copySync(frameworkBinaryPath);
}
// Copy the dSYM
if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) {
final File dsymOutputBinary = environment
.outputDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App');
dsymOutputBinary.parent.createSync(recursive: true);
environment
.buildDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App')
.copySync(dsymOutputBinary.path);
}
// Copy the assets.
final Depfile assetDepfile = await copyAssets(
environment,
assetDirectory,
targetPlatform: TargetPlatform.ios,
shaderTarget: ShaderTarget.sksl,
);
final DepfileService depfileService = DepfileService(
fileSystem: environment.fileSystem,
logger: environment.logger,
);
depfileService.writeToFile(
assetDepfile,
environment.buildDir.childFile('flutter_assets.d'),
);
// Copy the plist from either the project or module.
// TODO(zanderso): add plist to inputs
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
flutterProject.ios.appFrameworkInfoPlist
.copySync(environment.outputDir
.childDirectory('App.framework')
.childFile('Info.plist').path);
_signFramework(environment, frameworkBinaryPath, buildMode);
}
}
/// Build a debug iOS application bundle.
class DebugIosApplicationBundle extends IosAssetBundle {
const DebugIosApplicationBundle();
@override
String get name => 'debug_ios_bundle_flutter_assets';
@override
List<Source> get inputs => <Source>[
const Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug),
const Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug),
const Source.pattern('{BUILD_DIR}/app.dill'),
...super.inputs,
];
@override
List<Source> get outputs => <Source>[
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/vm_snapshot_data'),
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/isolate_snapshot_data'),
const Source.pattern('{OUTPUT_DIR}/App.framework/flutter_assets/kernel_blob.bin'),
...super.outputs,
];
@override
List<Target> get dependencies => <Target>[
const DebugUniversalFramework(),
...super.dependencies,
];
}
/// IosAssetBundle with debug symbols, used for Profile and Release builds.
abstract class _IosAssetBundleWithDSYM extends IosAssetBundle {
const _IosAssetBundleWithDSYM();
@override
List<Source> get inputs => <Source>[
...super.inputs,
const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
List<Source> get outputs => <Source>[
...super.outputs,
const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
}
/// Build a profile iOS application bundle.
class ProfileIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ProfileIosApplicationBundle();
@override
String get name => 'profile_ios_bundle_flutter_assets';
@override
List<Target> get dependencies => const <Target>[
AotAssemblyProfile(),
];
}
/// Build a release iOS application bundle.
class ReleaseIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ReleaseIosApplicationBundle();
@override
String get name => 'release_ios_bundle_flutter_assets';
@override
List<Target> get dependencies => const <Target>[
AotAssemblyRelease(),
];
@override
Future<void> build(Environment environment) async {
bool buildSuccess = true;
try {
await super.build(environment);
} catch (_) { // ignore: avoid_catches_without_on_clauses
buildSuccess = false;
rethrow;
} finally {
// Send a usage event when the app is being archived.
// Since assemble is run during a `flutter build`/`run` as well as an out-of-band
// archive command from Xcode, this is a more accurate count than `flutter build ipa` alone.
if (environment.defines[kXcodeAction]?.toLowerCase() == 'install') {
environment.logger.printTrace('Sending archive event if usage enabled.');
UsageEvent(
'assemble',
'ios-archive',
label: buildSuccess ? 'success' : 'fail',
flutterUsage: environment.usage,
).send();
}
}
}
}
/// Create an App.framework for debug iOS targets.
///
/// This framework needs to exist for the Xcode project to link/bundle,
/// but it isn't actually executed. To generate something valid, we compile a trivial
/// constant.
Future<void> _createStubAppFramework(File outputFile, Environment environment,
Set<String>? iosArchNames, String sdkRoot) async {
try {
outputFile.createSync(recursive: true);
} on Exception catch (e) {
throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
}
final FileSystem fileSystem = environment.fileSystem;
final Directory tempDir = fileSystem.systemTempDirectory
.createTempSync('flutter_tools_stub_source.');
try {
final File stubSource = tempDir.childFile('debug_app.cc')
..writeAsStringSync(r'''
static const int Moo = 88;
''');
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, fileSystem);
await globals.xcode!.clang(<String>[
'-x',
'c',
for (String arch in iosArchNames ?? <String>{}) ...<String>['-arch', arch],
stubSource.path,
'-dynamiclib',
// Keep version in sync with AOTSnapshotter flag
if (environmentType == EnvironmentType.physical)
'-miphoneos-version-min=11.0'
else
'-miphonesimulator-version-min=11.0',
'-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
'-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
'-install_name', '@rpath/App.framework/App',
'-isysroot', sdkRoot,
'-o', outputFile.path,
]);
} finally {
try {
tempDir.deleteSync(recursive: true);
} on FileSystemException {
// Best effort. Sometimes we can't delete things from system temp.
} on Exception catch (e) {
throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e');
}
}
_signFramework(environment, outputFile.path, BuildMode.debug);
}
void _signFramework(Environment environment, String binaryPath, BuildMode buildMode) {
String? codesignIdentity = environment.defines[kCodesignIdentity];
if (codesignIdentity == null || codesignIdentity.isEmpty) {
codesignIdentity = '-';
}
final ProcessResult result = environment.processManager.runSync(<String>[
'codesign',
'--force',
'--sign',
codesignIdentity,
if (buildMode != BuildMode.release) ...<String>[
// Mimic Xcode's timestamp codesigning behavior on non-release binaries.
'--timestamp=none',
],
binaryPath,
]);
if (result.exitCode != 0) {
throw Exception('Failed to codesign $binaryPath with identity $codesignIdentity.\n${result.stderr}');
}
}