blob: 47aa614e049ebbc140c7e799214b91e09eb7def5 [file] [log] [blame]
// Copyright 2019 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.
// Note: this Builder does not run in the same process as the flutter_tool, so
// the DI provided getters such as `fs` will not work.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:build/build.dart';
import 'package:package_config/packages_file.dart' as packages_file;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
const String _kFlutterDillOutputExtension = '.app.dill';
const String _kPackagesExtension = '.packages';
const String _kMultirootScheme = 'org-dartlang-app';
/// A builder which creates a kernel and packages file for a Flutter app.
///
/// Unlike the package:build kernel builders, this creates a single kernel from
/// dart source using the frontend server binary. The newly created .package
/// file replaces the relative root of the current package with a multi-root
/// which includes the generated directory.
class FlutterKernelBuilder implements Builder {
const FlutterKernelBuilder({
@required this.disabled,
@required this.mainPath,
@required this.aot,
@required this.trackWidgetCreation,
@required this.targetProductVm,
@required this.linkPlatformKernelIn,
@required this.extraFrontEndOptions,
@required this.sdkRoot,
@required this.packagesPath,
@required this.incrementalCompilerByteStorePath,
@required this.frontendServerPath,
@required this.engineDartBinaryPath,
});
/// The path to the entrypoint that will be compiled.
final String mainPath;
/// The path to the pub generated .packages file.
final String packagesPath;
/// The path to the root of the flutter patched SDK.
final String sdkRoot;
/// The path to the frontend server snapshot.
final String frontendServerPath;
/// The path to the dart executable to use to run the frontend server
/// snapshot.
final String engineDartBinaryPath;
/// Whether to build an ahead of time build.
final bool aot;
/// Whether to disable production of kernel.
final bool disabled;
/// Whether the `trackWidgetCreation` flag is provided to the frontend
/// server.
final bool trackWidgetCreation;
/// Whether to provide the Dart product define to the frontend server.
final bool targetProductVm;
/// When in batch mode, link platform kernel file into result kernel file.
final bool linkPlatformKernelIn;
/// Whether to compile incrementally.
final String incrementalCompilerByteStorePath;
/// Additional arguments to pass to the frontend server.
final List<String> extraFrontEndOptions;
@override
Map<String, List<String>> get buildExtensions => const <String, List<String>>{
'.dart': <String>[_kFlutterDillOutputExtension, _kPackagesExtension],
};
@override
Future<void> build(BuildStep buildStep) async {
// Do not resolve dependencies if this does not correspond to the main
// entrypoint. Do not generate kernel if it has been disabled.
if (!mainPath.contains(buildStep.inputId.path) || disabled) {
return;
}
final AssetId outputId = buildStep.inputId.changeExtension(_kFlutterDillOutputExtension);
final AssetId packagesOutputId = buildStep.inputId.changeExtension(_kPackagesExtension);
// Create a scratch space file that can be read/written by the frontend server.
// It is okay to hard-code these file names because we will copy them back
// from the temp directory at the end of the build step.
final Directory tempDirecory = await Directory.systemTemp.createTemp('_flutter_build');
final File packagesFile = File(path.join(tempDirecory.path, _kPackagesExtension));
final File outputFile = File(path.join(tempDirecory.path, 'main.app.dill'));
await outputFile.create();
await packagesFile.create();
final Directory projectDir = File(packagesPath).parent;
final String packageName = buildStep.inputId.package;
final String oldPackagesContents = await File(packagesPath).readAsString();
// Note: currently we only replace the root package with a multiroot
// scheme. To support codegen on arbitrary packages we will need to do
// this for each dependency.
final String newPackagesContents = oldPackagesContents.replaceFirst('$packageName:lib/', '$packageName:$_kMultirootScheme:/');
await packagesFile.writeAsString(newPackagesContents);
String absoluteMainPath;
if (path.isAbsolute(mainPath)) {
absoluteMainPath = mainPath;
} else {
absoluteMainPath = path.join(projectDir.absolute.path, mainPath);
}
// start up the frontend server with configuration.
final List<String> arguments = <String>[
frontendServerPath,
'--sdk-root',
sdkRoot,
'--strong',
'--target=flutter',
];
if (trackWidgetCreation) {
arguments.add('--track-widget-creation');
}
if (!linkPlatformKernelIn) {
arguments.add('--no-link-platform');
}
if (aot) {
arguments.add('--aot');
arguments.add('--tfa');
}
if (targetProductVm) {
arguments.add('-Ddart.vm.product=true');
}
if (incrementalCompilerByteStorePath != null) {
arguments.add('--incremental');
}
final String generatedRoot = path.join(projectDir.absolute.path, '.dart_tool', 'build', 'generated', '$packageName', 'lib${Platform.pathSeparator}');
final String normalRoot = path.join(projectDir.absolute.path, 'lib${Platform.pathSeparator}');
arguments.addAll(<String>[
'--packages',
Uri.file(packagesFile.path).toString(),
'--output-dill',
outputFile.path,
'--filesystem-root',
normalRoot,
'--filesystem-root',
generatedRoot,
'--filesystem-scheme',
_kMultirootScheme,
]);
if (extraFrontEndOptions != null) {
arguments.addAll(extraFrontEndOptions);
}
final Uri mainUri = _PackageUriMapper.findUri(
absoluteMainPath,
packagesFile.path,
_kMultirootScheme,
<String>[normalRoot, generatedRoot],
);
arguments.add(mainUri?.toString() ?? absoluteMainPath);
// Invoke the frontend server and copy the dill back to the output
// directory.
try {
final Process server = await Process.start(engineDartBinaryPath, arguments);
final _StdoutHandler _stdoutHandler = _StdoutHandler();
server.stderr
.transform<String>(utf8.decoder)
.listen(log.shout);
server.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(_stdoutHandler.handler);
await server.exitCode;
await _stdoutHandler.compilerOutput.future;
await buildStep.writeAsBytes(outputId, await outputFile.readAsBytes());
await buildStep.writeAsBytes(packagesOutputId, await packagesFile.readAsBytes());
} catch (err, stackTrace) {
log.shout('frontend server failed to start: $err, $stackTrace');
}
}
}
class _StdoutHandler {
_StdoutHandler() {
reset();
}
bool compilerMessageReceived = false;
String boundaryKey;
Completer<_CompilerOutput> compilerOutput;
bool _suppressCompilerMessages;
void handler(String message) {
const String kResultPrefix = 'result ';
if (boundaryKey == null) {
if (message.startsWith(kResultPrefix))
boundaryKey = message.substring(kResultPrefix.length);
} else if (message.startsWith(boundaryKey)) {
if (message.length <= boundaryKey.length) {
compilerOutput.complete(null);
return;
}
final int spaceDelimiter = message.lastIndexOf(' ');
compilerOutput.complete(
_CompilerOutput(
message.substring(boundaryKey.length + 1, spaceDelimiter),
int.parse(message.substring(spaceDelimiter + 1).trim())));
} else if (!_suppressCompilerMessages) {
if (compilerMessageReceived == false) {
log.info('\nCompiler message:');
compilerMessageReceived = true;
}
log.info(message);
}
}
// This is needed to get ready to process next compilation result output,
// with its own boundary key and new completer.
void reset({bool suppressCompilerMessages = false}) {
boundaryKey = null;
compilerMessageReceived = false;
compilerOutput = Completer<_CompilerOutput>();
_suppressCompilerMessages = suppressCompilerMessages;
}
}
class _CompilerOutput {
const _CompilerOutput(this.outputFilename, this.errorCount);
final String outputFilename;
final int errorCount;
}
/// Converts filesystem paths to package URIs.
class _PackageUriMapper {
_PackageUriMapper(String scriptPath, String packagesPath, String fileSystemScheme, List<String> fileSystemRoots) {
final List<int> bytes = File(path.absolute(packagesPath)).readAsBytesSync();
final Map<String, Uri> packageMap = packages_file.parse(bytes, Uri.file(packagesPath, windows: Platform.isWindows));
final String scriptUri = Uri.file(scriptPath, windows: Platform.isWindows).toString();
for (String packageName in packageMap.keys) {
final String prefix = packageMap[packageName].toString();
if (fileSystemScheme != null && fileSystemRoots != null && prefix.contains(fileSystemScheme)) {
_packageName = packageName;
_uriPrefixes = fileSystemRoots
.map((String name) => Uri.file(name, windows: Platform.isWindows).toString())
.toList();
return;
}
if (scriptUri.startsWith(prefix)) {
_packageName = packageName;
_uriPrefixes = <String>[prefix];
return;
}
}
}
String _packageName;
List<String> _uriPrefixes;
Uri map(String scriptPath) {
if (_packageName == null) {
return null;
}
final String scriptUri = Uri.file(scriptPath, windows: Platform.isWindows).toString();
for (String uriPrefix in _uriPrefixes) {
if (scriptUri.startsWith(uriPrefix)) {
return Uri.parse('package:$_packageName/${scriptUri.substring(uriPrefix.length)}');
}
}
return null;
}
static Uri findUri(String scriptPath, String packagesPath, String fileSystemScheme, List<String> fileSystemRoots) {
return _PackageUriMapper(scriptPath, packagesPath, fileSystemScheme, fileSystemRoots).map(scriptPath);
}
}