// 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 '../../artifacts.dart'; import '../../base/build.dart'; import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../base/process.dart'; import '../../build_info.dart'; import '../../globals.dart' as globals hide fs, logger, processManager, artifacts; import '../../project.dart'; import '../build_system.dart'; import '../depfile.dart'; import '../exceptions.dart'; import 'assets.dart'; import 'common.dart'; import 'icon_tree_shaker.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 build(Environment environment) async { final AOTSnapshotter snapshotter = AOTSnapshotter( reportTimings: false, fileSystem: environment.fileSystem, logger: environment.logger, xcode: globals.xcode, artifacts: environment.artifacts, processManager: environment.processManager, ); final String buildOutputPath = environment.buildDir.path; if (environment.defines[kBuildMode] == null) { throw MissingDefineException(kBuildMode, 'aot_assembly'); } if (environment.defines[kTargetPlatform] == null) { throw MissingDefineException(kTargetPlatform, 'aot_assembly'); } final List extraGenSnapshotOptions = decodeDartDefines(environment.defines, kExtraGenSnapshotOptions); final bool bitcode = environment.defines[kBitcodeFlag] == 'true'; final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; final List darwinArchs = environment.defines[kIosArchs] ?.split(' ') ?.map(getIOSArchForName) ?.toList() ?? [DarwinArch.arm64]; if (targetPlatform != TargetPlatform.ios) { throw Exception('aot_assembly is only supported for iOS applications.'); } if (darwinArchs.contains(DarwinArch.x86_64)) { throw Exception( 'release/profile builds are only supported for physical devices. ' 'attempted to build for $darwinArchs.' ); } final String codeSizeDirectory = environment.defines[kCodeSizeDirectory]; // If we're building multiple iOS archs the binaries need to be lipo'd // together. final List> pending = >[]; for (final DarwinArch darwinArch in darwinArchs) { final List archExtraGenSnapshotOptions = List.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: environment.defines[kSdkRoot], bitcode: bitcode, quiet: true, splitDebugInfo: splitDebugInfo, dartObfuscation: dartObfuscation, extraGenSnapshotOptions: archExtraGenSnapshotOptions, )); } final List results = await Future.wait(pending); if (results.any((int result) => result != 0)) { throw Exception('AOT snapshotter exited with code ${results.join()}'); } final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App'); environment.fileSystem.directory(resultPath).parent.createSync(recursive: true); final ProcessResult result = await environment.processManager.run([ 'lipo', ...darwinArchs.map((DarwinArch iosArch) => environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), '-create', '-output', resultPath, ]); if (result.exitCode != 0) { throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}'); } } } /// 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 get inputs => const [ Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'), Source.pattern('{BUILD_DIR}/app.dill'), Source.artifact(Artifact.engineDartBinary), Source.artifact(Artifact.skyEnginePath), // TODO(jonahwilliams): 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 get outputs => const [ Source.pattern('{OUTPUT_DIR}/App.framework/App'), ]; @override List get dependencies => const [ 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 get inputs => const [ Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'), Source.pattern('{BUILD_DIR}/app.dill'), Source.artifact(Artifact.engineDartBinary), Source.artifact(Artifact.skyEnginePath), // TODO(jonahwilliams): 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 get outputs => const [ Source.pattern('{OUTPUT_DIR}/App.framework/App'), ]; @override List get dependencies => const [ 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 get dependencies => const [ KernelSnapshot(), ]; @override List get inputs => const [ Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/ios.dart'), ]; @override List get outputs => const [ Source.pattern('{BUILD_DIR}/App.framework/App'), ]; @override Future build(Environment environment) async { // Generate a trivial App.framework. final Set iosArchNames = environment.defines[kIosArchs] ?.split(' ') ?.toSet(); final File output = environment.buildDir .childDirectory('App.framework') .childFile('App'); environment.buildDir.createSync(recursive: true); final RunResult createFrameworkResult = await createStubAppFramework( output, environment.defines[kSdkRoot], iosArchNames, ); if (createFrameworkResult.exitCode != 0) { throw Exception('Failed to create App.framework.'); } } } /// 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 get dependencies => const [ KernelSnapshot(), ]; @override List get inputs => const [ Source.pattern('{BUILD_DIR}/App.framework/App'), Source.pattern('{PROJECT_DIR}/pubspec.yaml'), ...IconTreeShaker.inputs, ]; @override List get outputs => const [ Source.pattern('{OUTPUT_DIR}/App.framework/App'), Source.pattern('{OUTPUT_DIR}/App.framework/Info.plist') ]; @override List get depfiles => [ 'flutter_assets.d', ]; @override Future build(Environment environment) async { if (environment.defines[kBuildMode] == null) { throw MissingDefineException(kBuildMode, name); } final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); 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(frameworkDirectory.childFile('App').path); 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(frameworkDirectory.childFile('App').path); } // Copy the assets. final Depfile assetDepfile = await copyAssets( environment, assetDirectory, targetPlatform: TargetPlatform.ios, ); 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(jonahwilliams): add plist to inputs final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir); final Directory plistRoot = flutterProject.isModule ? flutterProject.ios.ephemeralDirectory : environment.projectDir.childDirectory('ios'); plistRoot .childDirectory('Flutter') .childFile('AppFrameworkInfo.plist') .copySync(environment.outputDir .childDirectory('App.framework') .childFile('Info.plist').path); } } /// Build a debug iOS application bundle. class DebugIosApplicationBundle extends IosAssetBundle { const DebugIosApplicationBundle(); @override String get name => 'debug_ios_bundle_flutter_assets'; @override List get inputs => [ 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 get outputs => [ 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 get dependencies => [ const DebugUniversalFramework(), ...super.dependencies, ]; } /// Build a profile iOS application bundle. class ProfileIosApplicationBundle extends IosAssetBundle { const ProfileIosApplicationBundle(); @override String get name => 'profile_ios_bundle_flutter_assets'; @override List get dependencies => const [ AotAssemblyProfile(), ]; } /// Build a release iOS application bundle. class ReleaseIosApplicationBundle extends IosAssetBundle { const ReleaseIosApplicationBundle(); @override String get name => 'release_ios_bundle_flutter_assets'; @override List get dependencies => const [ AotAssemblyRelease(), ]; } /// 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 createStubAppFramework(File outputFile, String sdkRoot, Set iosArchNames) async { try { outputFile.createSync(recursive: true); } on Exception catch (e) { throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e'); } final Directory tempDir = outputFile.fileSystem.systemTempDirectory .createTempSync('flutter_tools_stub_source.'); try { final File stubSource = tempDir.childFile('debug_app.cc') ..writeAsStringSync(r''' static const int Moo = 88; '''); return await globals.xcode.clang([ '-x', 'c', for (String arch in iosArchNames) ...['-arch', arch], stubSource.path, '-dynamiclib', '-fembed-bitcode-marker', '-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'); } } }