// Copyright 2017 The Chromium 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 'dart:async'; import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../cache.dart'; import '../compile.dart'; import '../dart/package_map.dart'; import '../globals.dart'; import '../macos/xcode.dart'; import '../project.dart'; import '../usage.dart'; import 'context.dart'; import 'file_system.dart'; import 'fingerprint.dart'; import 'process.dart'; GenSnapshot get genSnapshot => context.get(); /// A snapshot build configuration. class SnapshotType { SnapshotType(this.platform, this.mode) : assert(mode != null); final TargetPlatform platform; final BuildMode mode; @override String toString() => '$platform $mode'; } /// Interface to the gen_snapshot command-line tool. class GenSnapshot { const GenSnapshot(); static String getSnapshotterPath(SnapshotType snapshotType) { return artifacts.getArtifactPath( Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode); } Future run({ @required SnapshotType snapshotType, IOSArch iosArch, Iterable additionalArgs = const [], }) { final List args = [ '--causal_async_stacks', ...additionalArgs, ]; final String snapshotterPath = getSnapshotterPath(snapshotType); // iOS gen_snapshot is a multi-arch binary. Running as an i386 binary will // generate armv7 code. Running as an x86_64 binary will generate arm64 // code. /usr/bin/arch can be used to run binaries with the specified // architecture. if (snapshotType.platform == TargetPlatform.ios) { final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64'; return runCommandAndStreamOutput(['/usr/bin/arch', hostArch, snapshotterPath, ...args]); } StringConverter outputFilter; if (additionalArgs.contains('--strip')) { // Filter out gen_snapshot's warning message about stripping debug symbols // from ELF library snapshots. const String kStripWarning = 'Warning: Generating ELF library without DWARF debugging information.'; outputFilter = (String line) => line != kStripWarning ? line : null; } return runCommandAndStreamOutput([snapshotterPath, ...args], mapFunction: outputFilter); } } class AOTSnapshotter { AOTSnapshotter({this.reportTimings = false}); /// If true then AOTSnapshotter would report timings for individual building /// steps (Dart front-end parsing and snapshot generation) in a stable /// machine readable form. See [AOTSnapshotter._timedStep]. final bool reportTimings; /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. Future build({ @required TargetPlatform platform, @required BuildMode buildMode, @required String mainPath, @required String packagesPath, @required String outputPath, IOSArch iosArch, List extraGenSnapshotOptions = const [], }) async { FlutterProject flutterProject; if (fs.file('pubspec.yaml').existsSync()) { flutterProject = FlutterProject.current(); } 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 PackageMap packageMap = PackageMap(packagesPath); final String packageMapError = packageMap.checkValid(); if (packageMapError != null) { printError(packageMapError); return 1; } final Directory outputDir = fs.directory(outputPath); outputDir.createSync(recursive: true); final String skyEnginePkg = _getPackagePath(packageMap, 'sky_engine'); final String uiPath = fs.path.join(skyEnginePkg, 'lib', 'ui', 'ui.dart'); final String vmServicePath = fs.path.join(skyEnginePkg, 'sdk_ext', 'vmservice_io.dart'); final List inputPaths = [uiPath, vmServicePath, mainPath]; final Set outputPaths = {}; final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d'); final List genSnapshotArgs = [ '--deterministic', ]; if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) { printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions'); genSnapshotArgs.addAll(extraGenSnapshotOptions); } final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S'); if (platform == TargetPlatform.ios) { // Assembly AOT snapshot. outputPaths.add(assembly); genSnapshotArgs.add('--snapshot_kind=app-aot-assembly'); genSnapshotArgs.add('--assembly=$assembly'); } else { final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so'); outputPaths.add(aotSharedLibrary); genSnapshotArgs.add('--snapshot_kind=app-aot-elf'); genSnapshotArgs.add('--elf=$aotSharedLibrary'); genSnapshotArgs.add('--strip'); } if (platform == TargetPlatform.android_arm || iosArch == IOSArch.armv7) { // Use softfp for Android armv7 devices. // This is the default for armv7 iOS builds, but harmless to set. // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489 genSnapshotArgs.add('--no-sim-use-hardfp'); // Not supported by the Pixel in 32-bit mode. genSnapshotArgs.add('--no-use-integer-division'); } genSnapshotArgs.add(mainPath); // Verify that all required inputs exist. final Iterable missingInputs = inputPaths.where((String p) => !fs.isFileSync(p)); if (missingInputs.isNotEmpty) { printError('Missing input files: $missingInputs from $inputPaths'); return 1; } // If inputs and outputs have not changed since last run, skip the build. final Fingerprinter fingerprinter = Fingerprinter( fingerprintPath: '$depfilePath.fingerprint', paths: [mainPath, ...inputPaths, ...outputPaths], properties: { 'buildMode': buildMode.toString(), 'targetPlatform': platform.toString(), 'entryPoint': mainPath, 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '), 'engineHash': Cache.instance.engineRevision, 'buildersUsed': '${flutterProject != null && flutterProject.hasBuilders}', }, depfilePaths: [], ); // TODO(jonahwilliams): re-enable once this can be proved correct. // if (await fingerprinter.doesFingerprintMatch()) { // printTrace('Skipping AOT snapshot build. Fingerprint match.'); // return 0; // } final SnapshotType snapshotType = SnapshotType(platform, buildMode); final int genSnapshotExitCode = await _timedStep('snapshot(CompileTime)', 'aot-snapshot', () => genSnapshot.run( snapshotType: snapshotType, additionalArgs: genSnapshotArgs, iosArch: iosArch, )); if (genSnapshotExitCode != 0) { printError('Dart snapshot generator failed with exit code $genSnapshotExitCode'); return genSnapshotExitCode; } // Write path to gen_snapshot, since snapshots have to be re-generated when we roll // the Dart SDK. final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType); await outputDir.childFile('gen_snapshot.d').writeAsString('gen_snapshot.d: $genSnapshotPath\n'); // 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(iosArch: iosArch, assemblyPath: assembly, outputPath: outputDir.path); if (result.exitCode != 0) return result.exitCode; } // Compute and record build fingerprint. await fingerprinter.writeFingerprint(); return 0; } /// Builds an iOS framework at [outputPath]/App.framework from the assembly /// source at [assemblyPath]. Future _buildIosFramework({ @required IOSArch iosArch, @required String assemblyPath, @required String outputPath, }) async { final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64'; printStatus('Building App.framework for $targetArch...'); final List commonBuildOptions = ['-arch', targetArch, '-miphoneos-version-min=8.0']; final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o'); final RunResult compileResult = await xcode.cc([...commonBuildOptions, '-c', assemblyPath, '-o', assemblyO]); if (compileResult.exitCode != 0) { printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}'); return compileResult; } final String frameworkDir = fs.path.join(outputPath, 'App.framework'); fs.directory(frameworkDir).createSync(recursive: true); final String appLib = fs.path.join(frameworkDir, 'App'); final List linkArgs = [ ...commonBuildOptions, '-dynamiclib', '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', '-install_name', '@rpath/App.framework/App', '-o', appLib, assemblyO, ]; final RunResult linkResult = await xcode.clang(linkArgs); if (linkResult.exitCode != 0) { printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}'); } return linkResult; } /// Compiles a Dart file to kernel. /// /// Returns the output kernel file path, or null on failure. Future compileKernel({ @required TargetPlatform platform, @required BuildMode buildMode, @required String mainPath, @required String packagesPath, @required String outputPath, @required bool trackWidgetCreation, List extraFrontEndOptions = const [], }) async { final FlutterProject flutterProject = FlutterProject.current(); final Directory outputDir = fs.directory(outputPath); outputDir.createSync(recursive: true); printTrace('Compiling Dart to kernel: $mainPath'); if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty) printTrace('Extra front-end options: $extraFrontEndOptions'); final String depfilePath = fs.path.join(outputPath, 'kernel_compile.d'); final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject); final CompilerOutput compilerOutput = await _timedStep('frontend(CompileTime)', 'aot-kernel', () => kernelCompiler.compile( sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), mainPath: mainPath, packagesPath: packagesPath, outputFilePath: getKernelPathForTransformerOptions( fs.path.join(outputPath, 'app.dill'), trackWidgetCreation: trackWidgetCreation, ), depFilePath: depfilePath, extraFrontEndOptions: extraFrontEndOptions, linkPlatformKernelIn: true, aot: true, trackWidgetCreation: trackWidgetCreation, targetProductVm: buildMode == BuildMode.release, )); // Write path to frontend_server, since things need to be re-generated when that changes. final String frontendPath = artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk); await fs.directory(outputPath).childFile('frontend_server.d').writeAsString('frontend_server.d: $frontendPath\n'); return compilerOutput?.outputFilename; } bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) { if (buildMode == BuildMode.debug) return false; return const [ TargetPlatform.android_arm, TargetPlatform.android_arm64, TargetPlatform.ios, ].contains(platform); } String _getPackagePath(PackageMap packageMap, String package) { return fs.path.dirname(fs.path.fromUri(packageMap.map[package])); } /// This method is used to measure duration of an action and emit it into /// verbose output from flutter_tool for other tools (e.g. benchmark runner) /// to find. /// Important: external performance tracking tools expect format of this /// output to be stable. Future _timedStep(String marker, String analyticsVar, FutureOr Function() action) async { final Stopwatch sw = Stopwatch()..start(); final T value = await action(); if (reportTimings) { printStatus('$marker: ${sw.elapsedMilliseconds} ms.'); } flutterUsage.sendTiming('build', analyticsVar, Duration(milliseconds: sw.elapsedMilliseconds)); return value; } } class JITSnapshotter { /// Builds a JIT VM snapshot of the specified kernel. This snapshot includes /// data as well as either machine code or DBC, depending on build configuration. Future build({ @required TargetPlatform platform, @required BuildMode buildMode, @required String mainPath, @required String packagesPath, @required String outputPath, @required String compilationTraceFilePath, List extraGenSnapshotOptions = const [], }) async { if (!_isValidJitPlatform(platform)) { printError('${getNameForTargetPlatform(platform)} does not support JIT snapshotting.'); return 1; } final Directory outputDir = fs.directory(outputPath); outputDir.createSync(recursive: true); final String engineVmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: buildMode); final String engineIsolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: buildMode); final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data'); final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr'); final List inputPaths = [ mainPath, compilationTraceFilePath, engineVmSnapshotData, engineIsolateSnapshotData, ]; final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d'); final List genSnapshotArgs = [ '--deterministic', ]; if (buildMode == BuildMode.debug) { genSnapshotArgs.add('--enable_asserts'); } if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) { printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions'); genSnapshotArgs.addAll(extraGenSnapshotOptions); } final Set outputPaths = { isolateSnapshotData, isolateSnapshotInstructions, }; // There are a couple special cases below where we create a snapshot // with only the data section, which only contains interpreted code. bool supportsAppJit = true; if (platform == TargetPlatform.android_x64 && getCurrentHostPlatform() == HostPlatform.windows_x64) { supportsAppJit = false; printStatus('Android x64 dynamic build on Windows x64 will use purely interpreted ' 'code for now (see https://github.com/flutter/flutter/issues/17489).'); } if (platform == TargetPlatform.android_x86) { supportsAppJit = false; printStatus('Android x86 dynamic build will use purely interpreted code for now. ' 'To optimize performance, consider using --target-platform=android-x64.'); } genSnapshotArgs.addAll([ '--snapshot_kind=${supportsAppJit ? 'app-jit' : 'app'}', '--load_compilation_trace=$compilationTraceFilePath', '--load_vm_snapshot_data=$engineVmSnapshotData', '--load_isolate_snapshot_data=$engineIsolateSnapshotData', '--isolate_snapshot_data=$isolateSnapshotData', ]); genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions'); if (platform == TargetPlatform.android_arm) { // Use softfp for Android armv7 devices. // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489 genSnapshotArgs.add('--no-sim-use-hardfp'); // Not supported by the Pixel in 32-bit mode. genSnapshotArgs.add('--no-use-integer-division'); } genSnapshotArgs.add(mainPath); // Verify that all required inputs exist. final Iterable missingInputs = inputPaths.where((String p) => !fs.isFileSync(p)); if (missingInputs.isNotEmpty) { printError('Missing input files: $missingInputs from $inputPaths'); return 1; } // If inputs and outputs have not changed since last run, skip the build. final Fingerprinter fingerprinter = Fingerprinter( fingerprintPath: '$depfilePath.fingerprint', paths: [mainPath, ...inputPaths, ...outputPaths], properties: { 'buildMode': buildMode.toString(), 'targetPlatform': platform.toString(), 'entryPoint': mainPath, 'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '), }, depfilePaths: [], ); // TODO(jonahwilliams): re-enable once this can be proved correct. // if (await fingerprinter.doesFingerprintMatch()) { // printTrace('Skipping JIT snapshot build. Fingerprint match.'); // return 0; // } final SnapshotType snapshotType = SnapshotType(platform, buildMode); final int genSnapshotExitCode = await genSnapshot.run( snapshotType: snapshotType, additionalArgs: genSnapshotArgs, ); if (genSnapshotExitCode != 0) { printError('Dart snapshot generator failed with exit code $genSnapshotExitCode'); return genSnapshotExitCode; } // Write path to gen_snapshot, since snapshots have to be re-generated when we roll // the Dart SDK. final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType); await outputDir.childFile('gen_snapshot.d').writeAsString('gen_snapshot.d: $genSnapshotPath\n'); // Compute and record build fingerprint. await fingerprinter.writeFingerprint(); return 0; } bool _isValidJitPlatform(TargetPlatform platform) { return const [ TargetPlatform.android_arm, TargetPlatform.android_arm64, TargetPlatform.android_x86, TargetPlatform.android_x64, ].contains(platform); } }