blob: dfab8d34e423dcbf6344c417a19effeb0fd24a66 [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.
import 'dart:async';
import 'dart:convert';
import 'package:usage/uuid/uuid.dart';
import 'artifacts.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart';
import 'base/process_manager.dart';
import 'globals.dart';
KernelCompiler get kernelCompiler => context[KernelCompiler];
typedef void CompilerMessageConsumer(String message);
class CompilerOutput {
String outputFilename;
int errorCount;
CompilerOutput(this.outputFilename, this.errorCount);
}
class _StdoutHandler {
_StdoutHandler({this.consumer: printError}) {
reset();
}
final CompilerMessageConsumer consumer;
String boundaryKey;
Completer<CompilerOutput> compilerOutput;
void handler(String string) {
const String kResultPrefix = 'result ';
if (boundaryKey == null) {
if (string.startsWith(kResultPrefix))
boundaryKey = string.substring(kResultPrefix.length);
} else if (string.startsWith(boundaryKey)) {
if (string.length <= boundaryKey.length) {
compilerOutput.complete(null);
return;
}
final int spaceDelimiter = string.lastIndexOf(' ');
compilerOutput.complete(
new CompilerOutput(
string.substring(boundaryKey.length + 1, spaceDelimiter),
int.parse(string.substring(spaceDelimiter + 1).trim())));
}
else
consumer('compiler message: $string');
}
// This is needed to get ready to process next compilation result output,
// with its own boundary key and new completer.
void reset() {
boundaryKey = null;
compilerOutput = new Completer<CompilerOutput>();
}
}
class KernelCompiler {
const KernelCompiler();
Future<CompilerOutput> compile(
{String sdkRoot,
String mainPath,
String outputFilePath,
String depFilePath,
bool linkPlatformKernelIn: false,
bool aot: false,
List<String> entryPointsJsonFiles,
bool trackWidgetCreation: false,
List<String> extraFrontEndOptions,
String incrementalCompilerByteStorePath,
String packagesPath,
List<String> fileSystemRoots,
String fileSystemScheme}) async {
final String frontendServer = artifacts.getArtifactPath(
Artifact.frontendServerSnapshotForEngineDartSdk
);
// This is a URI, not a file path, so the forward slash is correct even on Windows.
if (!sdkRoot.endsWith('/'))
sdkRoot = '$sdkRoot/';
final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
if (!processManager.canRun(engineDartPath)) {
throwToolExit('Unable to find Dart binary at $engineDartPath');
}
final List<String> command = <String>[
engineDartPath,
frontendServer,
'--sdk-root',
sdkRoot,
'--strong',
'--target=flutter',
];
if (trackWidgetCreation)
command.add('--track-widget-creation');
if (!linkPlatformKernelIn)
command.add('--no-link-platform');
if (aot) {
command.add('--aot');
command.add('--tfa');
}
if (entryPointsJsonFiles != null) {
for (String entryPointsJson in entryPointsJsonFiles) {
command.addAll(<String>['--entry-points', entryPointsJson]);
}
}
if (incrementalCompilerByteStorePath != null) {
command.add('--incremental');
}
if (packagesPath != null) {
command.addAll(<String>['--packages', packagesPath]);
}
if (outputFilePath != null) {
command.addAll(<String>['--output-dill', outputFilePath]);
}
if (depFilePath != null && (fileSystemRoots == null || fileSystemRoots.isEmpty)) {
command.addAll(<String>['--depfile', depFilePath]);
}
if (fileSystemRoots != null) {
for (String root in fileSystemRoots) {
command.addAll(<String>['--filesystem-root', root]);
}
}
if (fileSystemScheme != null) {
command.addAll(<String>['--filesystem-scheme', fileSystemScheme]);
}
if (extraFrontEndOptions != null)
command.addAll(extraFrontEndOptions);
command.add(mainPath);
printTrace(command.join(' '));
final Process server = await processManager
.start(command)
.catchError((dynamic error, StackTrace stack) {
printError('Failed to start frontend server $error, $stack');
});
final _StdoutHandler stdoutHandler = new _StdoutHandler();
server.stderr
.transform(utf8.decoder)
.listen((String s) { printError('compiler message: $s'); });
server.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(stdoutHandler.handler);
final int exitCode = await server.exitCode;
return exitCode == 0 ? stdoutHandler.compilerOutput.future : null;
}
}
/// Wrapper around incremental frontend server compiler, that communicates with
/// server via stdin/stdout.
///
/// The wrapper is intended to stay resident in memory as user changes, reloads,
/// restarts the Flutter app.
class ResidentCompiler {
ResidentCompiler(this._sdkRoot, {bool trackWidgetCreation: false,
String packagesPath, List<String> fileSystemRoots, String fileSystemScheme ,
CompilerMessageConsumer compilerMessageConsumer: printError})
: assert(_sdkRoot != null),
_trackWidgetCreation = trackWidgetCreation,
_packagesPath = packagesPath,
_fileSystemRoots = fileSystemRoots,
_fileSystemScheme = fileSystemScheme,
stdoutHandler = new _StdoutHandler(consumer: compilerMessageConsumer) {
// This is a URI, not a file path, so the forward slash is correct even on Windows.
if (!_sdkRoot.endsWith('/'))
_sdkRoot = '$_sdkRoot/';
}
final bool _trackWidgetCreation;
final String _packagesPath;
final List<String> _fileSystemRoots;
final String _fileSystemScheme;
String _sdkRoot;
Process _server;
final _StdoutHandler stdoutHandler;
/// If invoked for the first time, it compiles Dart script identified by
/// [mainPath], [invalidatedFiles] list is ignored.
/// On successive runs [invalidatedFiles] indicates which files need to be
/// recompiled. If [mainPath] is [null], previously used [mainPath] entry
/// point that is used for recompilation.
/// Binary file name is returned if compilation was successful, otherwise
/// null is returned.
Future<CompilerOutput> recompile(String mainPath, List<String> invalidatedFiles,
{String outputPath, String packagesFilePath}) async {
stdoutHandler.reset();
// First time recompile is called we actually have to compile the app from
// scratch ignoring list of invalidated files.
if (_server == null)
return _compile(_mapFilename(mainPath), outputPath, _mapFilename(packagesFilePath));
final String inputKey = new Uuid().generateV4();
_server.stdin.writeln('recompile ${mainPath != null ? _mapFilename(mainPath) + " ": ""}$inputKey');
for (String fileUri in invalidatedFiles) {
_server.stdin.writeln(_mapFileUri(fileUri));
}
_server.stdin.writeln(inputKey);
return stdoutHandler.compilerOutput.future;
}
Future<CompilerOutput> _compile(String scriptFilename, String outputPath,
String packagesFilePath) async {
final String frontendServer = artifacts.getArtifactPath(
Artifact.frontendServerSnapshotForEngineDartSdk
);
final List<String> args = <String>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
frontendServer,
'--sdk-root',
_sdkRoot,
'--incremental',
'--strong',
'--target=flutter',
];
if (outputPath != null) {
args.addAll(<String>['--output-dill', outputPath]);
}
if (packagesFilePath != null) {
args.addAll(<String>['--packages', packagesFilePath]);
}
if (_trackWidgetCreation) {
args.add('--track-widget-creation');
}
if (_packagesPath != null) {
args.addAll(<String>['--packages', _packagesPath]);
}
if (_fileSystemRoots != null) {
for (String root in _fileSystemRoots) {
args.addAll(<String>['--filesystem-root', root]);
}
}
if (_fileSystemScheme != null) {
args.addAll(<String>['--filesystem-scheme', _fileSystemScheme]);
}
_server = await processManager.start(args);
_server.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
stdoutHandler.handler,
onDone: () {
// when outputFilename future is not completed, but stdout is closed
// process has died unexpectedly.
if (!stdoutHandler.compilerOutput.isCompleted) {
stdoutHandler.compilerOutput.complete(null);
}
});
_server.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String s) { printError('compiler message: $s'); });
_server.stdin.writeln('compile $scriptFilename');
return stdoutHandler.compilerOutput.future;
}
/// Should be invoked when results of compilation are accepted by the client.
///
/// Either [accept] or [reject] should be called after every [recompile] call.
void accept() {
_server.stdin.writeln('accept');
}
/// Should be invoked when results of compilation are rejected by the client.
///
/// Either [accept] or [reject] should be called after every [recompile] call.
void reject() {
_server.stdin.writeln('reject');
}
/// Should be invoked when frontend server compiler should forget what was
/// accepted previously so that next call to [recompile] produces complete
/// kernel file.
void reset() {
_server.stdin.writeln('reset');
}
String _mapFilename(String filename) {
if (_fileSystemRoots != null) {
for (String root in _fileSystemRoots) {
if (filename.startsWith(root)) {
return new Uri(
scheme: _fileSystemScheme, path: filename.substring(root.length))
.toString();
}
}
}
return filename;
}
String _mapFileUri(String fileUri) {
if (_fileSystemRoots != null) {
final String filename = Uri.parse(fileUri).toFilePath();
for (String root in _fileSystemRoots) {
if (filename.startsWith(root)) {
return new Uri(
scheme: _fileSystemScheme, path: filename.substring(root.length))
.toString();
}
}
}
return fileUri;
}
Future<dynamic> shutdown() {
_server.kill();
return _server.exitCode;
}
}