blob: 15ee47f8285373d139328c01deecfc28d0b470d0 [file] [log] [blame]
// 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.
library frontend_server;
import 'dart:async';
import 'dart:convert';
import 'dart:io' hide FileSystemEntity;
import 'package:args/args.dart';
// front_end/src imports below that require lint `ignore_for_file`
// are a temporary state of things until frontend team builds better api
// that would replace api used below. This api was made private in
// an effort to discourage further use.
// ignore_for_file: implementation_imports
import 'package:front_end/src/api_prototype/compiler_options.dart';
import 'package:front_end/src/api_prototype/file_system.dart'
show FileSystemEntity;
// Use of multi_root_file_system.dart directly from front_end package is a
// temporarily solution while we are looking for better home for that
// functionality.
import 'package:front_end/src/multi_root_file_system.dart';
import 'package:kernel/ast.dart';
import 'package:kernel/binary/ast_to_binary.dart';
import 'package:kernel/binary/limited_ast_to_binary.dart';
import 'package:kernel/kernel.dart' show Component, loadComponentFromBytes;
import 'package:kernel/target/targets.dart';
import 'package:path/path.dart' as path;
import 'package:usage/uuid/uuid.dart';
import 'package:vm/incremental_compiler.dart' show IncrementalCompiler;
import 'package:vm/kernel_front_end.dart' show compileToKernel;
ArgParser argParser = new ArgParser(allowTrailingOptions: true)
..addFlag('train',
help: 'Run through sample command line to produce snapshot',
negatable: false)
..addFlag('incremental',
help: 'Run compiler in incremental mode', defaultsTo: false)
..addOption('sdk-root',
help: 'Path to sdk root',
defaultsTo: '../../out/android_debug/flutter_patched_sdk')
..addOption('platform', help: 'Platform kernel filename')
..addFlag('aot',
help: 'Run compiler in AOT mode (enables whole-program transformations)',
defaultsTo: false)
..addFlag('strong',
help: 'Run compiler in strong mode (uses strong mode semantics)',
defaultsTo: false)
..addFlag('tfa',
help:
'Enable global type flow analysis and related transformations in AOT mode.',
defaultsTo: false)
..addMultiOption('entry-points',
help: 'Path to JSON file with the list of entry points')
..addFlag('link-platform',
help:
'When in batch mode, link platform kernel file into result kernel file.'
' Intended use is to satisfy different loading strategies implemented'
' by gen_snapshot(which needs platform embedded) vs'
' Flutter engine(which does not)',
defaultsTo: true)
..addOption('output-dill',
help: 'Output path for the generated dill', defaultsTo: null)
..addOption('output-incremental-dill',
help: 'Output path for the generated incremental dill', defaultsTo: null)
..addOption('depfile',
help: 'Path to output Ninja depfile. Only used in batch mode.')
..addOption('packages',
help: '.packages file to use for compilation', defaultsTo: null)
..addOption('target',
help: 'Target model that determines what core libraries are available',
allowed: <String>['vm', 'flutter'],
defaultsTo: 'vm')
..addMultiOption('filesystem-root',
help: 'File path that is used as a root in virtual filesystem used in'
' compiled kernel files. When used --output-dill should be provided'
' as well.',
hide: true)
..addOption('filesystem-scheme',
help:
'Scheme that is used in virtual filesystem set up via --filesystem-root'
' option',
defaultsTo: 'org-dartlang-root',
hide: true);
String usage = '''
Usage: server [options] [input.dart]
If input dart source code is provided on the command line, then the server
compiles it, generates dill file and exits.
If no input dart source is provided on the command line, server waits for
instructions from stdin.
Instructions:
- compile <input.dart>
- recompile [<input.dart>] <boundary-key>
<path/to/updated/file1.dart>
<path/to/updated/file2.dart>
...
<boundary-key>
- accept
- quit
Output:
- result <boundary-key>
<compiler output>
<boundary-key> [<output.dill>]
Options:
${argParser.usage}
''';
enum _State { READY_FOR_INSTRUCTION, RECOMPILE_LIST }
/// Actions that every compiler should implement.
abstract class CompilerInterface {
/// Compile given Dart program identified by `filename` with given list of
/// `options`. When `generator` parameter is omitted, new instance of
/// `IncrementalKernelGenerator` is created by this method. Main use for this
/// parameter is for mocking in tests.
Future<Null> compile(
String filename,
ArgResults options, {
IncrementalCompiler generator,
});
/// Assuming some Dart program was previously compiled, recompile it again
/// taking into account some changed(invalidated) sources.
Future<Null> recompileDelta({String filename});
/// Accept results of previous compilation so that next recompilation cycle
/// won't recompile sources that were previously reported as changed.
void acceptLastDelta();
/// This let's compiler know that source file identifed by `uri` was changed.
void invalidate(Uri uri);
/// Resets incremental compiler accept/reject status so that next time
/// recompile is requested, complete kernel file is produced.
void resetIncrementalCompiler();
}
abstract class ProgramTransformer {
void transform(Component component);
}
/// Class that for test mocking purposes encapsulates creation of [BinaryPrinter].
class BinaryPrinterFactory {
/// Creates new [BinaryPrinter] to write to [targetSink].
BinaryPrinter newBinaryPrinter(IOSink targetSink) {
return new LimitedBinaryPrinter(targetSink, (_) => true /* predicate */,
false /* excludeUriToSource */);
}
}
class FrontendCompiler implements CompilerInterface {
FrontendCompiler(this._outputStream,
{this.printerFactory, this.transformer}) {
_outputStream ??= stdout;
printerFactory ??= new BinaryPrinterFactory();
}
StringSink _outputStream;
BinaryPrinterFactory printerFactory;
CompilerOptions _compilerOptions;
Uri _mainSource;
ArgResults _options;
IncrementalCompiler _generator;
String _kernelBinaryFilename;
String _kernelBinaryFilenameIncremental;
String _kernelBinaryFilenameFull;
final ProgramTransformer transformer;
void setMainSourceFilename(String filename) {
final Uri filenameUri = _getFileOrUri(filename);
_mainSource = filenameUri;
}
@override
Future<Null> compile(
String filename,
ArgResults options, {
IncrementalCompiler generator,
}) async {
_options = options;
setMainSourceFilename(filename);
_kernelBinaryFilenameFull = _options['output-dill'] ?? '$filename.dill';
_kernelBinaryFilenameIncremental = _options['output-incremental-dill'] ??
(_options['output-dill'] != null
? '${_options["output-dill"]}.incremental.dill'
: '$filename.incremental.dill');
_kernelBinaryFilename = _kernelBinaryFilenameFull;
final String boundaryKey = new Uuid().generateV4();
_outputStream.writeln('result $boundaryKey');
final Uri sdkRoot = _ensureFolderPath(options['sdk-root']);
final String platformKernelDill = options['platform'] ??
(options['strong'] ? 'platform_strong.dill' : 'platform.dill');
final CompilerOptions compilerOptions = new CompilerOptions()
..sdkRoot = sdkRoot
..packagesFileUri = _getFileOrUri(_options['packages'])
..strongMode = options['strong']
..sdkSummary = sdkRoot.resolve(platformKernelDill)
..reportMessages = true;
if (options.wasParsed('filesystem-root')) {
List<Uri> rootUris = <Uri>[];
for (String root in options['filesystem-root']) {
rootUris.add(Uri.base.resolveUri(new Uri.file(root)));
}
compilerOptions.fileSystem = new MultiRootFileSystem(
options['filesystem-scheme'], rootUris, compilerOptions.fileSystem);
if (_options['output-dill'] == null) {
print("When --filesystem-root is specified it is required to specify"
" --output-dill option that points to physical file system location"
" of a target dill file.");
exit(1);
}
}
final TargetFlags targetFlags =
new TargetFlags(strongMode: options['strong']);
compilerOptions.target = getTarget(options['target'], targetFlags);
Component component;
if (options['incremental']) {
_compilerOptions = compilerOptions;
_generator = generator ??
_createGenerator(new Uri.file(_kernelBinaryFilenameFull));
await invalidateIfBootstrapping();
component = await _runWithPrintRedirection(() => _generator.compile());
} else {
if (options['link-platform']) {
// TODO(aam): Remove linkedDependencies once platform is directly embedded
// into VM snapshot and http://dartbug.com/30111 is fixed.
compilerOptions.linkedDependencies = <Uri>[
sdkRoot.resolve(platformKernelDill)
];
}
component = await _runWithPrintRedirection(() => compileToKernel(
_mainSource, compilerOptions,
aot: options['aot'],
useGlobalTypeFlowAnalysis: options['tfa'],
entryPoints: options['entry-points']));
}
if (component != null) {
if (transformer != null) {
transformer.transform(component);
}
final IOSink sink = new File(_kernelBinaryFilename).openWrite();
final BinaryPrinter printer = printerFactory.newBinaryPrinter(sink);
printer.writeComponentFile(component);
await sink.close();
_outputStream.writeln('$boundaryKey $_kernelBinaryFilename');
final String depfile = options['depfile'];
if (depfile != null) {
await _writeDepfile(component, _kernelBinaryFilename, depfile);
}
_kernelBinaryFilename = _kernelBinaryFilenameIncremental;
} else
_outputStream.writeln(boundaryKey);
return null;
}
Future<Null> invalidateIfBootstrapping() async {
if (_kernelBinaryFilename != _kernelBinaryFilenameFull) return null;
try {
final File f = new File(_kernelBinaryFilenameFull);
if (!f.existsSync()) return null;
final Component component = loadComponentFromBytes(f.readAsBytesSync());
for (Uri uri in component.uriToSource.keys) {
if ('$uri' == '') continue;
final List<int> oldBytes = component.uriToSource[uri].source;
final FileSystemEntity entity =
_compilerOptions.fileSystem.entityForUri(uri);
if (!await entity.exists()) {
_generator.invalidate(uri);
continue;
}
final List<int> newBytes = await entity.readAsBytes();
if (oldBytes.length != newBytes.length) {
_generator.invalidate(uri);
continue;
}
for (int i = 0; i < oldBytes.length; ++i) {
if (oldBytes[i] != newBytes[i]) {
_generator.invalidate(uri);
continue;
}
}
}
} catch (e) {
// If there's a failure in the above block we might not have invalidated
// correctly. Create a new generator that doesn't bootstrap to avoid missing
// any changes.
_generator = _createGenerator(null);
}
}
@override
Future<Null> recompileDelta({String filename}) async {
final String boundaryKey = new Uuid().generateV4();
_outputStream.writeln('result $boundaryKey');
await invalidateIfBootstrapping();
if (filename != null) {
setMainSourceFilename(filename);
}
final Component deltaProgram =
await _generator.compile(entryPoint: _mainSource);
if (deltaProgram != null && transformer != null) {
transformer.transform(deltaProgram);
}
final IOSink sink = new File(_kernelBinaryFilename).openWrite();
final BinaryPrinter printer = printerFactory.newBinaryPrinter(sink);
printer.writeComponentFile(deltaProgram);
await sink.close();
_outputStream.writeln('$boundaryKey $_kernelBinaryFilename');
_kernelBinaryFilename = _kernelBinaryFilenameIncremental;
return null;
}
@override
void acceptLastDelta() {
_generator.accept();
}
@override
void invalidate(Uri uri) {
_generator.invalidate(uri);
}
@override
void resetIncrementalCompiler() {
_generator = _createGenerator(new Uri.file(_kernelBinaryFilenameFull));
_kernelBinaryFilename = _kernelBinaryFilenameFull;
}
Uri _getFileOrUri(String fileOrUri) {
if (fileOrUri == null) {
return null;
}
if (_options.wasParsed('filesystem-root')) {
// This is a hack.
// Only expect uri when filesystem-root option is specified. It has to
// be uri for filesystem-root use case because mapping is done on
// scheme-basis.
// This is so that we don't deal with Windows files paths that can not
// be processed as uris.
return Uri.base.resolve(fileOrUri);
}
return Uri.base.resolveUri(new Uri.file(fileOrUri));
}
IncrementalCompiler _createGenerator(Uri bootstrapDill) {
return new IncrementalCompiler(_compilerOptions, _mainSource,
bootstrapDill: bootstrapDill);
}
Uri _ensureFolderPath(String path) {
String uriPath = new Uri.file(path).toString();
if (!uriPath.endsWith('/')) {
uriPath = '$uriPath/';
}
return Uri.base.resolve(uriPath);
}
/// Runs the given function [f] in a Zone that redirects all prints into
/// [_outputStream].
Future<T> _runWithPrintRedirection<T>(Future<T> f()) {
return runZoned(() => new Future<T>(f),
zoneSpecification: new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) =>
_outputStream.writeln(line)));
}
}
String _escapePath(String path) {
return path.replaceAll(r'\', r'\\').replaceAll(r' ', r'\ ');
}
// https://ninja-build.org/manual.html#_depfile
void _writeDepfile(Component component, String output, String depfile) async {
final IOSink file = new File(depfile).openWrite();
file.write(_escapePath(output));
file.write(':');
for (Uri dep in component.uriToSource.keys) {
file.write(' ');
file.write(_escapePath(dep.toFilePath()));
}
file.write('\n');
await file.close();
}
/// Listens for the compilation commands on [input] stream.
/// This supports "interactive" recompilation mode of execution.
void listenAndCompile(CompilerInterface compiler, Stream<List<int>> input,
ArgResults options, void quit(),
{IncrementalCompiler generator}) {
_State state = _State.READY_FOR_INSTRUCTION;
String boundaryKey;
String recompileFilename;
input
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String string) async {
switch (state) {
case _State.READY_FOR_INSTRUCTION:
const String COMPILE_INSTRUCTION_SPACE = 'compile ';
const String RECOMPILE_INSTRUCTION_SPACE = 'recompile ';
if (string.startsWith(COMPILE_INSTRUCTION_SPACE)) {
final String filename =
string.substring(COMPILE_INSTRUCTION_SPACE.length);
await compiler.compile(filename, options, generator: generator);
} else if (string.startsWith(RECOMPILE_INSTRUCTION_SPACE)) {
// 'recompile [<filename>] <boundarykey>'
// where <boundarykey> can't have spaces
final String remainder =
string.substring(RECOMPILE_INSTRUCTION_SPACE.length);
final int spaceDelim = remainder.lastIndexOf(' ');
if (spaceDelim > -1) {
recompileFilename = remainder.substring(0, spaceDelim);
boundaryKey = remainder.substring(spaceDelim + 1);
} else {
boundaryKey = remainder;
}
state = _State.RECOMPILE_LIST;
} else if (string == 'accept') {
compiler.acceptLastDelta();
} else if (string == 'reset') {
compiler.resetIncrementalCompiler();
} else if (string == 'quit') {
quit();
}
break;
case _State.RECOMPILE_LIST:
if (string == boundaryKey) {
compiler.recompileDelta(filename: recompileFilename);
state = _State.READY_FOR_INSTRUCTION;
} else
compiler.invalidate(Uri.base.resolve(string));
break;
}
});
}
/// Entry point for this module, that creates `_FrontendCompiler` instance and
/// processes user input.
/// `compiler` is an optional parameter so it can be replaced with mocked
/// version for testing.
Future<int> starter(
List<String> args, {
CompilerInterface compiler,
Stream<List<int>> input,
StringSink output,
IncrementalCompiler generator,
BinaryPrinterFactory binaryPrinterFactory,
}) async {
ArgResults options;
try {
options = argParser.parse(args);
} catch (error) {
print('ERROR: $error\n');
print(usage);
return 1;
}
if (options['train']) {
final String sdkRoot = options['sdk-root'];
final String platform = options['platform'];
final Directory temp =
Directory.systemTemp.createTempSync('train_frontend_server');
try {
final String outputTrainingDill = path.join(temp.path, 'app.dill');
final List<String> args = <String>[
'--incremental',
'--sdk-root=$sdkRoot',
'--output-dill=$outputTrainingDill',
];
if (platform != null) {
args.add('--platform=${new Uri.file(platform)}');
}
options = argParser.parse(args);
compiler ??=
new FrontendCompiler(output, printerFactory: binaryPrinterFactory);
await compiler.compile(Platform.script.toFilePath(), options,
generator: generator);
compiler.acceptLastDelta();
await compiler.recompileDelta();
compiler.acceptLastDelta();
compiler.resetIncrementalCompiler();
await compiler.recompileDelta();
compiler.acceptLastDelta();
await compiler.recompileDelta();
compiler.acceptLastDelta();
return 0;
} finally {
temp.deleteSync(recursive: true);
}
}
compiler ??= new FrontendCompiler(
output,
printerFactory: binaryPrinterFactory,
);
if (options.rest.isNotEmpty) {
await compiler.compile(options.rest[0], options, generator: generator);
return 0;
}
listenAndCompile(compiler, input ?? stdin, options, () {
exit(0);
}, generator: generator);
return 0;
}