// 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/file_system.dart'; import 'base/io.dart'; import 'base/process_manager.dart'; import 'globals.dart'; String _dartExecutable() { final String engineDartSdkPath = artifacts.getArtifactPath( Artifact.engineDartSdkPath ); if (!fs.isDirectorySync(engineDartSdkPath)) { throwToolExit('No dart sdk Flutter host engine build found at $engineDartSdkPath.\n' 'Note that corresponding host engine build is required even when targeting particular device platforms.', exitCode: 2); } return fs.path.join(engineDartSdkPath, 'bin', 'dart'); } class _StdoutHandler { _StdoutHandler() { reset(); } String boundaryKey; Completer outputFilename; 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)) outputFilename.complete(string.length > boundaryKey.length ? string.substring(boundaryKey.length + 1) : null); else printError('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; outputFilename = new Completer(); } } Future compile( {String sdkRoot, String mainPath, bool linkPlatformKernelIn: false, bool aot: false, bool strongMode: false, List extraFrontEndOptions, String incrementalCompilerByteStorePath, String packagesPath}) 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 List command = [ _dartExecutable(), frontendServer, '--sdk-root', sdkRoot, ]; if (!linkPlatformKernelIn) command.add('--no-link-platform'); if (aot) { command.add('--aot'); } if (strongMode) { command.add('--strong'); } if (incrementalCompilerByteStorePath != null) { command.add('--incremental'); } if (packagesPath != null) { command.addAll(['--packages', packagesPath]); } if (extraFrontEndOptions != null) command.addAll(extraFrontEndOptions); command.add(mainPath); 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.outputFilename.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 strongMode: false}) : assert(_sdkRoot != null) { // This is a URI, not a file path, so the forward slash is correct even on Windows. if (!_sdkRoot.endsWith('/')) _sdkRoot = '$_sdkRoot/'; _strongMode = strongMode; } String _sdkRoot; bool _strongMode; Process _server; final _StdoutHandler stdoutHandler = new _StdoutHandler(); /// If invoked for the first time, it compiles Dart script identified by /// [mainPath], [invalidatedFiles] list is ignored. /// Otherwise, [mainPath] is ignored, but [invalidatedFiles] is recompiled /// into new binary. /// Binary file name is returned if compilation was successful, otherwise /// null is returned. Future recompile(String mainPath, List invalidatedFiles) 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(mainPath); final String inputKey = new Uuid().generateV4(); _server.stdin.writeln('recompile $inputKey'); invalidatedFiles.forEach(_server.stdin.writeln); _server.stdin.writeln(inputKey); return stdoutHandler.outputFilename.future; } Future _compile(String scriptFilename) async { final String frontendServer = artifacts.getArtifactPath( Artifact.frontendServerSnapshotForEngineDartSdk ); final List args = [ _dartExecutable(), frontendServer, '--sdk-root', _sdkRoot, '--incremental' ]; if (_strongMode) { args.add('--strong'); } _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.outputFilename.isCompleted) { stdoutHandler.outputFilename.complete(null); } }); _server.stderr .transform(UTF8.decoder) .transform(const LineSplitter()) .listen((String s) { printError('compiler message: $s'); }); _server.stdin.writeln('compile $scriptFilename'); return stdoutHandler.outputFilename.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'); } }