Chris Bracken 66c7b6a935
Add Fingerprinter class (#17255)
Adds a Fingerprinter utility class that can be used to compute unique
fingerprints for a set of input paths and build options, compare to the
output of a previous run, and skip the build action if no inputs or
options have changed. The existing Fingerprint class still does all the
heavy lifting. Fingerprinter adds common operations such as
reading/writing/comparing fingerprints and parsing depfiles.

This migrates existing uses of Fingerprint over to Fingerprinter.

This also adds better fingerprinting to AOT snapshotting, which
previously failed to include several options in its fingerprint
(--preview-dart-2, --prefer-shared-library).
2018-05-03 17:31:37 -07:00

387 lines
15 KiB
Dart

// 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 '../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 'context.dart';
import 'file_system.dart';
import 'fingerprint.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<int> run({
@required SnapshotType snapshotType,
@required String packagesPath,
@required String depfilePath,
Iterable<String> additionalArgs: const <String>[],
}) {
final List<String> args = <String>[
'--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(<String>['/usr/bin/arch', '-x86_64', snapshotterPath]..addAll(args));
}
return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
}
}
/// 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 ScriptSnapshotter {
/// Builds an architecture-independent snapshot of the specified script.
Future<int> build({
@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<String> args = <String>[
'--snapshot_kind=script',
'--script_snapshot=$snapshotPath',
'--vm_snapshot_data=$vmSnapshotData',
'--isolate_snapshot_data=$isolateSnapshotData',
'--enable-mirrors=false',
mainPath,
];
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: '$depfilePath.fingerprint',
paths: <String>[
mainPath,
snapshotPath,
vmSnapshotData,
isolateSnapshotData,
],
properties: <String, String>{
'buildMode': snapshotType.mode.toString(),
'targetPlatform': snapshotType.platform?.toString() ?? '',
'entryPoint': mainPath,
},
depfilePaths: <String>[depfilePath],
);
if (await fingerprinter.doesFingerprintMatch()) {
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 fingerprinter.writeFingerprint();
return exitCode;
}
}
class AOTSnapshotter {
/// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
Future<int> build({
@required TargetPlatform platform,
@required BuildMode buildMode,
@required String mainPath,
@required String packagesPath,
@required String outputPath,
@required bool previewDart2,
@required bool preferSharedLibrary,
List<String> extraGenSnapshotOptions: const <String>[],
}) 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<String> inputPaths = <String>[uiPath, vmServicePath, vmEntryPoints, ioEntryPoints, mainPath];
final Set<String> outputPaths = new Set<String>();
final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
final List<String> genSnapshotArgs = <String>[
'--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(<String>[
'--reify-generic-functions',
'--strong',
]);
}
if (buildMode != BuildMode.release) {
genSnapshotArgs.addAll(<String>[
'--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 vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');
final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]);
genSnapshotArgs.addAll(<String>[
'--snapshot_kind=app-aot-blobs',
'--vm_snapshot_data=$vmSnapshotData',
'--isolate_snapshot_data=$isolateSnapshotData',
'--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<String> 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 Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: '$depfilePath.fingerprint',
paths: <String>[mainPath]..addAll(inputPaths)..addAll(outputPaths),
properties: <String, String>{
'buildMode': buildMode.toString(),
'targetPlatform': platform.toString(),
'entryPoint': mainPath,
'dart2': previewDart2.toString(),
'sharedLib': compileToSharedLibrary.toString(),
'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
},
depfilePaths: <String>[depfilePath],
);
if (await fingerprinter.doesFingerprintMatch()) {
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) {
final RunResult result = await _buildIosFramework(assemblyPath: assembly, outputPath: outputDir.path);
if (result.exitCode != 0)
return result.exitCode;
} else if (compileToSharedLibrary) {
final RunResult result = await _buildAndroidSharedLibrary(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<RunResult> _buildIosFramework({
@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 assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
final RunResult compileResult = await xcode.cc(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO]));
if (compileResult.exitCode != 0)
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<String> linkArgs = commonBuildOptions.toList()..addAll(<String>[
'-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);
return linkResult;
}
/// Builds an Android shared library at [outputPath]/app.so from the assembly
/// source at [assemblyPath].
Future<RunResult> _buildAndroidSharedLibrary({
@required String assemblyPath,
@required String outputPath,
}) async {
// 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(outputPath, 'app.so');
return await runCheckedAsync(<String>[androidSdk.ndkCompiler]
..addAll(androidSdk.ndkCompilerArgs)
..addAll(<String>[ '-shared', '-nostdlib', '-o', assemblySo, assemblyPath ]));
}
/// Compiles a Dart file to kernel.
///
/// Returns the output kernel file path, or null on failure.
Future<String> compileKernel({
@required TargetPlatform platform,
@required BuildMode buildMode,
@required String mainPath,
@required String outputPath,
List<String> extraFrontEndOptions: const <String>[],
}) async {
final Directory outputDir = fs.directory(outputPath);
outputDir.createSync(recursive: true);
printTrace('Compiling Dart to kernel: $mainPath');
final List<String> entryPointsJsonFiles = <String>[
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;
}
bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
if (platform == TargetPlatform.ios && buildMode == BuildMode.debug)
return false;
return const <TargetPlatform>[
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]));
}
}