blob: 97d0578baac32ed6aa4ca0c8ecf02696a91c13ce [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// This library is a wrapper around the Dart to JavaScript (dart2js) compiler.
library services.compiler;
import 'dart:async';
import 'dart:io';
import 'package:bazel_worker/driver.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'common.dart';
import 'flutter_web.dart';
import 'pub.dart';
import 'sdk_manager.dart';
Logger _logger = Logger('compiler');
/// An interface to the dart2js compiler. A compiler object can process one
/// compile at a time.
class Compiler {
final FlutterSdk _sdk;
final FlutterWebManager _flutterWebManager;
final String _dartdevcPath;
final BazelWorkerDriver _ddcDriver;
Compiler(this._sdk)
: _dartdevcPath = path.join(_sdk.sdkPath, 'bin', 'dartdevc'),
_ddcDriver = BazelWorkerDriver(
() => Process.start(
path.join(_sdk.sdkPath, 'bin', 'dartdevc'),
<String>['--persistent_worker'],
),
maxWorkers: 1),
_flutterWebManager = FlutterWebManager();
bool importsOkForCompile(Set<String> imports) {
return !_flutterWebManager.hasUnsupportedImport(imports);
}
Future<CompilationResults> warmup({bool useHtml = false}) async {
return compile(useHtml ? sampleCodeWeb : sampleCode);
}
/// Compile the given string and return the resulting [CompilationResults].
Future<CompilationResults> compile(
String input, {
bool returnSourceMap = false,
}) async {
final imports = getAllImportsFor(input);
if (!importsOkForCompile(imports)) {
return CompilationResults(problems: <CompilationProblem>[
CompilationProblem._(
'unsupported import: ${_flutterWebManager.getUnsupportedImport(imports)}',
),
]);
}
final temp = await Directory.systemTemp.createTemp('dartpad');
_logger.info('Temp directory created: ${temp.path}');
try {
await copyPath(FlutterWebManager.dartTemplateProject.path, temp.path);
await Directory(path.join(temp.path, 'lib')).create(recursive: true);
final arguments = <String>[
'--suppress-hints',
'--terse',
if (!returnSourceMap) '--no-source-maps',
'--packages=${path.join('.dart_tool', 'package_config.json')}',
...['-o', '$kMainDart.js'],
path.join('lib', kMainDart),
];
final compileTarget = path.join(temp.path, 'lib', kMainDart);
final mainDart = File(compileTarget);
await mainDart.writeAsString(input);
final mainJs = File(path.join(temp.path, '$kMainDart.js'));
final mainSourceMap = File(path.join(temp.path, '$kMainDart.js.map'));
final dart2JSPath = path.join(_sdk.sdkPath, 'bin', 'dart2js');
_logger.info('About to exec: $dart2JSPath ${arguments.join(' ')}');
final result = await Process.run(dart2JSPath, arguments,
workingDirectory: temp.path);
if (result.exitCode != 0) {
final results = CompilationResults(problems: <CompilationProblem>[
CompilationProblem._(result.stdout as String),
]);
return results;
} else {
String sourceMap;
if (returnSourceMap && await mainSourceMap.exists()) {
sourceMap = await mainSourceMap.readAsString();
}
final results = CompilationResults(
compiledJS: await mainJs.readAsString(),
sourceMap: sourceMap,
);
return results;
}
} catch (e, st) {
_logger.warning('Compiler failed: $e\n$st');
rethrow;
} finally {
await temp.delete(recursive: true);
_logger.info('temp folder removed: ${temp.path}');
}
}
/// Compile the given string and return the resulting [DDCCompilationResults].
Future<DDCCompilationResults> compileDDC(String input) async {
final imports = getAllImportsFor(input);
if (!importsOkForCompile(imports)) {
return DDCCompilationResults.failed(<CompilationProblem>[
CompilationProblem._(
'unsupported import: ${_flutterWebManager.getUnsupportedImport(imports)}',
),
]);
}
final temp = await Directory.systemTemp.createTemp('dartpad');
_logger.info('Temp directory created: ${temp.path}');
try {
final usingFlutter = _flutterWebManager.usesFlutterWeb(imports);
if (usingFlutter) {
await copyPath(
FlutterWebManager.flutterTemplateProject.path, temp.path);
} else {
await copyPath(FlutterWebManager.dartTemplateProject.path, temp.path);
}
await Directory(path.join(temp.path, 'lib')).create(recursive: true);
final mainPath = path.join(temp.path, 'lib', kMainDart);
final bootstrapPath = path.join(temp.path, 'lib', kBootstrapDart);
final bootstrapContents =
usingFlutter ? kBootstrapFlutterCode : kBootstrapDartCode;
await File(bootstrapPath).writeAsString(bootstrapContents);
await File(mainPath).writeAsString(input);
final arguments = <String>[
'--modules=amd',
if (usingFlutter) ...[
'-s',
_flutterWebManager.summaryFilePath,
'-s',
'${_sdk.flutterBinPath}/cache/flutter_web_sdk/flutter_web_sdk/kernel/flutter_ddc_sdk.dill'
],
...['-o', path.join(temp.path, '$kMainDart.js')],
...['--module-name', 'dartpad_main'],
bootstrapPath,
'--packages=${path.join(temp.path, '.dart_tool', 'package_config.json')}',
];
final mainJs = File(path.join(temp.path, '$kMainDart.js'));
_logger.info('About to exec "$_dartdevcPath ${arguments.join(' ')}"');
final response =
await _ddcDriver.doWork(WorkRequest()..arguments.addAll(arguments));
if (response.exitCode != 0) {
return DDCCompilationResults.failed(<CompilationProblem>[
CompilationProblem._(response.output),
]);
} else {
// The `--single-out-file` option for dartdevc was removed in v2.7.0. As
// a result, the JS code produced above does *not* provide a name for
// the module it contains. That's a problem for DartPad, since it's
// adding the code to a script tag in an iframe rather than loading it
// as an individual file from baseURL. As a workaround, this replace
// statement injects a name into the module definition.
final processedJs = (await mainJs.readAsString())
.replaceFirst('define([', "define('dartpad_main', [");
final results = DDCCompilationResults(
compiledJS: processedJs,
modulesBaseUrl: 'https://storage.googleapis.com/'
'compilation_artifacts/${_sdk.versionFull}/',
);
return results;
}
} catch (e, st) {
_logger.warning('Compiler failed: $e\n$st');
rethrow;
} finally {
await temp.delete(recursive: true);
_logger.info('temp folder removed: ${temp.path}');
}
}
Future<void> dispose() async {
return _ddcDriver.terminateWorkers();
}
}
/// The result of a dart2js compile.
class CompilationResults {
final String compiledJS;
final String sourceMap;
final List<CompilationProblem> problems;
CompilationResults({
this.compiledJS,
this.problems = const <CompilationProblem>[],
this.sourceMap,
});
bool get hasOutput => compiledJS != null && compiledJS.isNotEmpty;
/// This is true if there were no errors.
bool get success => problems.isEmpty;
@override
String toString() => success
? 'CompilationResults: Success'
: 'Compilation errors: ${problems.join('\n')}';
}
/// The result of a DDC compile.
class DDCCompilationResults {
final String compiledJS;
final String modulesBaseUrl;
final List<CompilationProblem> problems;
DDCCompilationResults({this.compiledJS, this.modulesBaseUrl})
: problems = const <CompilationProblem>[];
DDCCompilationResults.failed(this.problems)
: compiledJS = null,
modulesBaseUrl = null;
bool get hasOutput => compiledJS != null && compiledJS.isNotEmpty;
/// This is true if there were no errors.
bool get success => problems.isEmpty;
@override
String toString() => success
? 'CompilationResults: Success'
: 'Compilation errors: ${problems.join('\n')}';
}
/// An issue associated with [CompilationResults].
class CompilationProblem implements Comparable<CompilationProblem> {
final String message;
CompilationProblem._(this.message);
@override
int compareTo(CompilationProblem other) => message.compareTo(other.message);
@override
String toString() => message;
}
/// Copies all of the files in the [from] directory to [to].
///
/// This is similar to `cp -R <from> <to>`:
/// * Symlinks are supported.
/// * Existing files are over-written, if any.
/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
/// * If [from] and [to] are canonically the same, no operation occurs.
///
/// Returns a future that completes when complete.
Future<Null> copyPath(String from, String to) async {
if (_doNothing(from, to)) {
return;
}
await Directory(to).create(recursive: true);
await for (final file in Directory(from).list(recursive: true)) {
final copyTo = path.join(to, path.relative(file.path, from: from));
if (file is Directory) {
await Directory(copyTo).create(recursive: true);
} else if (file is File) {
await File(file.path).copy(copyTo);
} else if (file is Link) {
await Link(copyTo).create(await file.target(), recursive: true);
}
}
}
bool _doNothing(String from, String to) {
if (path.canonicalize(from) == path.canonicalize(to)) {
return true;
}
if (path.isWithin(from, to)) {
throw ArgumentError('Cannot copy from $from to $to');
}
return false;
}