// 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 'dart:convert' show json; import 'package:crypto/crypto.dart' show md5; import 'package:meta/meta.dart'; import 'package:quiver/core.dart' show hash2; import '../android/android_sdk.dart'; import '../artifacts.dart'; import '../build_info.dart'; import '../compile.dart'; import '../dart/package_map.dart'; import '../globals.dart'; import '../ios/mac.dart'; import '../version.dart'; import 'context.dart'; import 'file_system.dart'; import 'process.dart'; GenSnapshot get genSnapshot => context[GenSnapshot]; /// 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(); Future run({ @required SnapshotType snapshotType, @required String packagesPath, @required String depfilePath, Iterable additionalArgs: const [], }) { final List args = [ '--await_is_keyword', '--causal_async_stacks', '--packages=$packagesPath', '--dependencies=$depfilePath', '--print_snapshot_sizes', ]..addAll(additionalArgs); final String snapshotterPath = artifacts.getArtifactPath(Artifact.genSnapshot, snapshotType.platform, snapshotType.mode); // 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) { // TODO(cbracken): for the moment, always generate only arm64 code. return runCommandAndStreamOutput(['/usr/bin/arch', '-x86_64', snapshotterPath]..addAll(args)); } return runCommandAndStreamOutput([snapshotterPath]..addAll(args)); } } /// A fingerprint for a set of build input files and properties. /// /// This class can be used during build actions to compute a fingerprint of the /// build action inputs, and if unchanged from the previous build, skip the /// build step. This assumes that build outputs are strictly a product of the /// fingerprint inputs. class Fingerprint { Fingerprint.fromBuildInputs(Map properties, Iterable inputPaths) { final Iterable files = inputPaths.map(fs.file); final Iterable missingInputs = files.where((File file) => !file.existsSync()); if (missingInputs.isNotEmpty) throw new ArgumentError('Missing input files:\n' + missingInputs.join('\n')); _checksums = {}; for (File file in files) { final List bytes = file.readAsBytesSync(); _checksums[file.path] = md5.convert(bytes).toString(); } _properties = {}..addAll(properties); } /// Creates a Fingerprint from serialized JSON. /// /// Throws [ArgumentError], if there is a version mismatch between the /// serializing framework and this framework. Fingerprint.fromJson(String jsonData) { final Map content = json.decode(jsonData); final String version = content['version']; if (version != FlutterVersion.instance.frameworkRevision) throw new ArgumentError('Incompatible fingerprint version: $version'); _checksums = content['files'] ?? {}; _properties = content['properties'] ?? {}; } Map _checksums; Map _properties; String toJson() => json.encode({ 'version': FlutterVersion.instance.frameworkRevision, 'properties': _properties, 'files': _checksums, }); @override bool operator==(dynamic other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; final Fingerprint typedOther = other; return _equalMaps(typedOther._checksums, _checksums) && _equalMaps(typedOther._properties, _properties); } bool _equalMaps(Map a, Map b) { return a.length == b.length && a.keys.every((String key) => a[key] == b[key]); } @override // Ignore map entries here to avoid becoming inconsistent with equals // due to differences in map entry order. int get hashCode => hash2(_properties.length, _checksums.length); } final RegExp _separatorExpr = new RegExp(r'([^\\]) '); final RegExp _escapeExpr = new RegExp(r'\\(.)'); /// Parses a VM snapshot dependency file. /// /// Snapshot dependency files are a single line mapping the output snapshot to a /// space-separated list of input files used to generate that output. Spaces and /// backslashes are escaped with a backslash. e.g, /// /// outfile : file1.dart fil\\e2.dart fil\ e3.dart /// /// will return a set containing: 'file1.dart', 'fil\e2.dart', 'fil e3.dart'. Future> readDepfile(String depfilePath) async { // Depfile format: // outfile1 outfile2 : file1.dart file2.dart file3.dart final String contents = await fs.file(depfilePath).readAsString(); final String dependencies = contents.split(': ')[1]; return dependencies .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n') .split('\n') .map((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim()) .where((String path) => path.isNotEmpty) .toSet(); } /// Dart snapshot builder. /// /// Builds Dart snapshots in one of three modes: /// * Script snapshot: architecture-independent snapshot of a Dart script /// and core libraries. /// * AOT snapshot: architecture-specific ahead-of-time compiled snapshot /// suitable for loading with `mmap`. /// * Assembly AOT snapshot: architecture-specific ahead-of-time compile to /// assembly suitable for compilation as a static or dynamic library. class Snapshotter { /// Builds an architecture-independent snapshot of the specified script. Future buildScriptSnapshot({ @required String mainPath, @required String snapshotPath, @required String depfilePath, @required String packagesPath }) async { final SnapshotType snapshotType = new SnapshotType(null, BuildMode.debug); final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData); final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData); final List args = [ '--snapshot_kind=script', '--script_snapshot=$snapshotPath', '--vm_snapshot_data=$vmSnapshotData', '--isolate_snapshot_data=$isolateSnapshotData', '--enable-mirrors=false', mainPath, ]; final String fingerprintPath = '$depfilePath.fingerprint'; final Set outputPaths = [snapshotPath].toSet(); if (!await _isBuildRequired(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath)) { printTrace('Skipping script snapshot build. Fingerprints match.'); return 0; } // Build the snapshot. final int exitCode = await genSnapshot.run( snapshotType: snapshotType, packagesPath: packagesPath, depfilePath: depfilePath, additionalArgs: args, ); if (exitCode != 0) return exitCode; await _writeFingerprint(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath); return exitCode; } /// 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 outputPath, List extraFrontEndOptions: const [], }) async { final Directory outputDir = fs.directory(outputPath); outputDir.createSync(recursive: true); printTrace('Compiling Dart to kernel: $mainPath'); final List entryPointsJsonFiles = [ artifacts.getArtifactPath(Artifact.entryPointsJson, platform, buildMode), artifacts.getArtifactPath(Artifact.entryPointsExtraJson, platform, buildMode), ]; if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty) printTrace('Extra front-end options: $extraFrontEndOptions'); final String depfilePath = fs.path.join(outputPath, 'kernel_compile.d'); final CompilerOutput compilerOutput = await kernelCompiler.compile( sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath), mainPath: mainPath, outputFilePath: fs.path.join(outputPath, 'app.dill'), depFilePath: depfilePath, extraFrontEndOptions: extraFrontEndOptions, linkPlatformKernelIn: true, aot: true, entryPointsJsonFiles: entryPointsJsonFiles, trackWidgetCreation: false, ); // 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; } /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. Future buildAotSnapshot({ @required TargetPlatform platform, @required BuildMode buildMode, @required String mainPath, @required String packagesPath, @required String outputPath, @required bool previewDart2, @required bool preferSharedLibrary, List extraGenSnapshotOptions: const [], }) async { if (!_isValidAotPlatform(platform, buildMode)) { printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.'); return -1; } final bool compileToSharedLibrary = preferSharedLibrary && androidSdk.ndkCompiler != null; if (preferSharedLibrary && !compileToSharedLibrary) { printStatus('Could not find NDK compiler. Not building in shared library mode.'); } final PackageMap packageMap = new PackageMap(packagesPath); final String packageMapError = packageMap.checkValid(); if (packageMapError != null) { printError(packageMapError); return -2; } 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 String vmEntryPoints = artifacts.getArtifactPath(Artifact.dartVmEntryPointsTxt, platform, buildMode); final String ioEntryPoints = artifacts.getArtifactPath(Artifact.dartIoEntriesTxt, platform, buildMode); final List inputPaths = [uiPath, vmServicePath, vmEntryPoints, ioEntryPoints, mainPath]; final Set outputPaths = new Set(); final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data'); final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data'); final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d'); final List genSnapshotArgs = [ '--vm_snapshot_data=$vmSnapshotData', '--isolate_snapshot_data=$isolateSnapshotData', '--url_mapping=dart:ui,$uiPath', '--url_mapping=dart:vmservice_io,$vmServicePath', '--embedder_entry_points_manifest=$vmEntryPoints', '--embedder_entry_points_manifest=$ioEntryPoints', '--dependencies=$depfilePath', ]; if (previewDart2) { genSnapshotArgs.addAll([ '--reify-generic-functions', '--strong', ]); } if (buildMode != BuildMode.release) { genSnapshotArgs.addAll([ '--no-checked', '--conditional_directives', ]); } 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 (compileToSharedLibrary || platform == TargetPlatform.ios) { // Assembly AOT snapshot. outputPaths.add(assembly); genSnapshotArgs.add('--snapshot_kind=app-aot-assembly'); genSnapshotArgs.add('--assembly=$assembly'); } else { // Blob AOT snapshot. final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr'); final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr'); outputPaths.addAll([vmSnapshotData, isolateSnapshotData]); genSnapshotArgs.addAll([ '--snapshot_kind=app-aot-blobs', '--vm_snapshot_instructions=$vmSnapshotInstructions', '--isolate_snapshot_instructions=$isolateSnapshotInstructions', ]); } if (platform == TargetPlatform.android_arm) { // 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 -3; } // If inputs and outputs have not changed since last run, skip the build. final String fingerprintPath = '$depfilePath.fingerprint'; final SnapshotType snapshotType = new SnapshotType(platform, buildMode); if (!await _isBuildRequired(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath)) { printTrace('Skipping AOT snapshot build. Fingerprint match.'); return 0; } final int genSnapshotExitCode = await genSnapshot.run( snapshotType: new SnapshotType(platform, buildMode), packagesPath: packageMap.packagesPath, depfilePath: depfilePath, additionalArgs: genSnapshotArgs, ); if (genSnapshotExitCode != 0) { printError('Dart snapshot generator failed with exit code $genSnapshotExitCode'); return -4; } // Write path to gen_snapshot, since snapshots have to be re-generated when we roll // the Dart SDK. await outputDir.childFile('gen_snapshot.d').writeAsString('snapshot.d: $genSnapshot\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) { printStatus('Building App.framework...'); const List commonBuildOptions = const ['-arch', 'arm64', '-miphoneos-version-min=8.0']; final String assemblyO = fs.path.join(outputDir.path, 'snapshot_assembly.o'); await xcode.cc(commonBuildOptions.toList()..addAll(['-c', assembly, '-o', assemblyO])); final String frameworkDir = fs.path.join(outputDir.path, 'App.framework'); fs.directory(frameworkDir).createSync(recursive: true); final String appLib = fs.path.join(frameworkDir, 'App'); final List linkArgs = commonBuildOptions.toList()..addAll([ '-dynamiclib', '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', '-install_name', '@rpath/App.framework/App', '-o', appLib, assemblyO, ]); await xcode.clang(linkArgs); } else { if (compileToSharedLibrary) { // A word of warning: Instead of compiling via two steps, to a .o file and // then to a .so file we use only one command. When using two commands // gcc will end up putting a .eh_frame and a .debug_frame into the shared // library. Without stripping .debug_frame afterwards, unwinding tools // based upon libunwind use just one and ignore the contents of the other // (which causes it to not look into the other section and therefore not // find the correct unwinding information). final String assemblySo = fs.path.join(outputDir.path, 'app.so'); await runCheckedAsync([androidSdk.ndkCompiler] ..addAll(androidSdk.ndkCompilerArgs) ..addAll([ '-shared', '-nostdlib', '-o', assemblySo, assembly ])); } } // Compute and record build fingerprint. await _writeFingerprint(snapshotType, outputPaths, depfilePath, mainPath, fingerprintPath); return 0; } bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) { if (platform == TargetPlatform.ios && 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])); } Future _isBuildRequired(SnapshotType type, Set outputPaths, String depfilePath, String mainPath, String fingerprintPath) async { final File fingerprintFile = fs.file(fingerprintPath); final List requiredFiles = [fingerprintPath, depfilePath]..addAll(outputPaths); if (!requiredFiles.every(fs.isFileSync)) return true; try { if (fingerprintFile.existsSync()) { final Fingerprint oldFingerprint = new Fingerprint.fromJson(await fingerprintFile.readAsString()); final Set inputFilePaths = await readDepfile(depfilePath)..add(mainPath)..addAll(outputPaths); final Fingerprint newFingerprint = createFingerprint(type, mainPath, inputFilePaths); return oldFingerprint != newFingerprint; } } catch (e) { // Log exception and continue, this step is a performance improvement only. printTrace('Rebuilding snapshot due to fingerprint check error: $e'); } return true; } Future _writeFingerprint(SnapshotType type, Set outputPaths, String depfilePath, String mainPath, String fingerprintPath) async { try { final Set inputFilePaths = await readDepfile(depfilePath) ..add(mainPath) ..addAll(outputPaths); final Fingerprint fingerprint = createFingerprint(type, mainPath, inputFilePaths); await fs.file(fingerprintPath).writeAsString(fingerprint.toJson()); } catch (e, s) { // Log exception and continue, this step is a performance improvement only. printStatus('Error during snapshot fingerprinting: $e\n$s'); } } static Fingerprint createFingerprint(SnapshotType type, String mainPath, Iterable inputFilePaths) { final Map properties = { 'buildMode': type.mode.toString(), 'targetPlatform': type.platform?.toString() ?? '', 'entryPoint': mainPath, }; final List pathsWithSnapshotData = inputFilePaths.toList() ..add(artifacts.getArtifactPath(Artifact.vmSnapshotData)) ..add(artifacts.getArtifactPath(Artifact.isolateSnapshotData)); return new Fingerprint.fromBuildInputs(properties, pathsWithSnapshotData); } }