blob: c30e7a9ad0bd777b7be376d5a42434a29e8a49b9 [file] [log] [blame]
// Copyright (c) 2018, 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.
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:analyzer/src/generated/engine.dart' show AnalysisEngine;
import 'package:args/args.dart';
import 'package:front_end/src/api_unstable/ddc.dart'
show InitializedCompilerState;
import 'package:path/path.dart' as path;
import 'module_builder.dart';
import '../analyzer/command.dart' as analyzer_compiler;
import '../analyzer/driver.dart' show CompilerAnalysisDriver;
import '../kernel/command.dart' as kernel_compiler;
/// Shared code between Analyzer and Kernel CLI interfaces.
///
/// This file should only implement functionality that does not depend on
/// Analyzer/Kernel imports.
/// Variables that indicate which libraries are available in dev compiler.
// TODO(jmesserly): provide an option to compile without dart:html & friends?
Map<String, String> sdkLibraryVariables = {
'dart.isVM': 'false',
'dart.library.async': 'true',
'dart.library.core': 'true',
'dart.library.collection': 'true',
'dart.library.convert': 'true',
// TODO(jmesserly): this is not really supported in dart4web other than
// `debugger()`
'dart.library.developer': 'true',
'dart.library.io': 'false',
'dart.library.isolate': 'false',
'dart.library.js': 'true',
'dart.library.js_util': 'true',
'dart.library.math': 'true',
'dart.library.mirrors': 'false',
'dart.library.typed_data': 'true',
'dart.library.indexed_db': 'true',
'dart.library.html': 'true',
'dart.library.html_common': 'true',
'dart.library.svg': 'true',
'dart.library.ui': 'false',
'dart.library.web_audio': 'true',
'dart.library.web_gl': 'true',
'dart.library.web_sql': 'true',
};
/// Shared compiler options between `dartdevc` kernel and analyzer backends.
class SharedCompilerOptions {
/// Whether to emit the source mapping file.
///
/// This supports debugging the original source code instead of the generated
/// code.
final bool sourceMap;
/// Whether to emit the source mapping file in the program text, so the
/// runtime can enable synchronous stack trace deobsfuscation.
final bool inlineSourceMap;
/// Whether to emit a summary file containing API signatures.
///
/// This is required for a modular build process.
final bool summarizeApi;
/// Whether to preserve metdata only accessible via mirrors.
final bool emitMetadata;
// Whether to enable assertions.
final bool enableAsserts;
/// Whether to compile code in a more permissive REPL mode allowing access
/// to private members across library boundaries.
///
/// This should only set `true` by our REPL compiler.
bool replCompile;
/// Mapping from absolute file paths to bazel short path to substitute in
/// source maps.
final Map<String, String> bazelMapping;
final Map<String, String> summaryModules;
final List<ModuleFormat> moduleFormats;
/// Experimental language features that are enabled/disabled, see
/// [the spec](https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md)
/// for more details.
final Map<String, bool> experiments;
/// The name of the module.
///
/// This used when to support file concatenation. The JS module will contain
/// its module name inside itself, allowing it to declare the module name
/// independently of the file.
String moduleName;
SharedCompilerOptions(
{this.sourceMap = true,
this.inlineSourceMap = false,
this.summarizeApi = true,
this.emitMetadata = false,
this.enableAsserts = true,
this.replCompile = false,
this.bazelMapping = const {},
this.summaryModules = const {},
this.moduleFormats = const [],
this.experiments = const {},
this.moduleName});
SharedCompilerOptions.fromArguments(ArgResults args,
[String moduleRoot, String summaryExtension])
: this(
sourceMap: args['source-map'] as bool,
inlineSourceMap: args['inline-source-map'] as bool,
summarizeApi: args['summarize'] as bool,
emitMetadata: args['emit-metadata'] as bool,
enableAsserts: args['enable-asserts'] as bool,
experiments:
_parseExperiments(args['enable-experiment'] as List<String>),
bazelMapping:
_parseBazelMappings(args['bazel-mapping'] as List<String>),
summaryModules: _parseCustomSummaryModules(
args['summary'] as List<String>, moduleRoot, summaryExtension),
moduleFormats: parseModuleFormatOption(args),
moduleName: _getModuleName(args, moduleRoot),
replCompile: args['repl-compile'] as bool);
static void addArguments(ArgParser parser, {bool hide = true}) {
addModuleFormatOptions(parser, hide: hide);
parser
..addMultiOption('summary',
abbr: 's',
help: 'summary file(s) of imported libraries, optionally\n'
'with module import path: -s path.sum=js/import/path')
..addMultiOption('enable-experiment',
help: 'used to enable/disable experimental language features',
hide: hide)
..addFlag('summarize',
help: 'emit an API summary file', defaultsTo: true, hide: hide)
..addFlag('source-map',
help: 'emit source mapping', defaultsTo: true, hide: hide)
..addFlag('inline-source-map',
help: 'emit source mapping inline', defaultsTo: false, hide: hide)
..addFlag('emit-metadata',
help: 'emit metadata annotations queriable via mirrors', hide: hide)
..addFlag('enable-asserts',
help: 'enable assertions', defaultsTo: true, hide: hide)
..addOption('module-name',
help: 'The output module name, used in some JS module formats.\n'
'Defaults to the output file name (without .js).')
// TODO(jmesserly): rename this, it has nothing to do with bazel.
..addMultiOption('bazel-mapping',
help: '--bazel-mapping=gen/to/library.dart,to/library.dart\n'
'adjusts the path in source maps.',
splitCommas: false,
hide: hide)
..addOption('library-root',
help: '(deprecated) used to name libraries inside the module, '
'ignored with -k.',
hide: hide)
..addFlag('repl-compile',
help: 'compile in a more permissive REPL mode, allowing access'
' to private members across library boundaries. This should'
' only be used by debugging tools.',
defaultsTo: false,
hide: hide);
}
static String _getModuleName(ArgResults args, String moduleRoot) {
var moduleName = args['module-name'] as String;
if (moduleName == null) {
var outPaths = args['out'];
var outPath = outPaths is String
? outPaths
: (outPaths as List<String>)
.firstWhere((_) => true, orElse: () => null);
// TODO(jmesserly): fix the debugger console so it's not passing invalid
// options.
if (outPath == null) return null;
if (moduleRoot != null) {
// TODO(jmesserly): remove this legacy support after a deprecation
// period. (Mainly this is to give time for migrating build rules.)
moduleName =
path.withoutExtension(path.relative(outPath, from: moduleRoot));
} else {
moduleName = path.basenameWithoutExtension(outPath);
}
}
// TODO(jmesserly): this should probably use sourcePathToUri.
//
// Also we should not need this logic if the user passed in the module name
// explicitly. It is here for backwards compatibility until we can confirm
// that build systems do not depend on passing windows-style paths here.
return path.toUri(moduleName).toString();
}
}
/// Finds explicit module names of the form `path=name` in [summaryPaths],
/// and returns the path to mapping in an ordered map from `path` to `name`.
///
/// A summary path can contain "=" followed by an explicit module name to
/// allow working with summaries whose physical location is outside of the
/// module root directory.
Map<String, String> _parseCustomSummaryModules(List<String> summaryPaths,
[String moduleRoot, String summaryExt]) {
var pathToModule = <String, String>{};
if (summaryPaths == null) return pathToModule;
for (var summaryPath in summaryPaths) {
var equalSign = summaryPath.indexOf("=");
String modulePath;
var summaryPathWithoutExt = summaryExt != null
? summaryPath.substring(
0,
// Strip off the extension, including the last `.`.
summaryPath.length - (summaryExt.length + 1))
: path.withoutExtension(summaryPath);
if (equalSign != -1) {
modulePath = summaryPath.substring(equalSign + 1);
summaryPath = summaryPath.substring(0, equalSign);
} else if (moduleRoot != null && path.isWithin(moduleRoot, summaryPath)) {
// TODO(jmesserly): remove this, it's legacy --module-root support.
modulePath = path.url.joinAll(
path.split(path.relative(summaryPathWithoutExt, from: moduleRoot)));
} else {
modulePath = path.basename(summaryPathWithoutExt);
}
pathToModule[summaryPath] = modulePath;
}
return pathToModule;
}
Map<String, bool> _parseExperiments(List<String> arguments) {
var result = <String, bool>{};
for (var argument in arguments) {
for (var feature in argument.split(',')) {
if (feature.startsWith('no-')) {
result[feature.substring(3)] = false;
} else {
result[feature] = true;
}
}
}
return result;
}
Map<String, String> _parseBazelMappings(List<String> argument) {
var mappings = <String, String>{};
for (var mapping in argument) {
var splitMapping = mapping.split(',');
if (splitMapping.length >= 2) {
mappings[path.absolute(splitMapping[0])] = splitMapping[1];
}
}
return mappings;
}
/// Taken from analyzer to implement `--ignore-unrecognized-flags`
List<String> filterUnknownArguments(List<String> args, ArgParser parser) {
if (!args.contains('--ignore-unrecognized-flags')) return args;
var knownOptions = new HashSet<String>();
var knownAbbreviations = new HashSet<String>();
parser.options.forEach((String name, Option option) {
knownOptions.add(name);
var abbreviation = option.abbr;
if (abbreviation != null) {
knownAbbreviations.add(abbreviation);
}
if (option.negatable) {
knownOptions.add('no-$name');
}
});
String optionName(int prefixLength, String arg) {
int equalsOffset = arg.lastIndexOf('=');
if (equalsOffset < 0) {
return arg.substring(prefixLength);
}
return arg.substring(prefixLength, equalsOffset);
}
var filtered = <String>[];
for (var arg in args) {
if (arg.startsWith('--') && arg.length > 2) {
if (knownOptions.contains(optionName(2, arg))) {
filtered.add(arg);
}
} else if (arg.startsWith('-') && arg.length > 1) {
if (knownAbbreviations.contains(optionName(1, arg))) {
filtered.add(arg);
}
} else {
filtered.add(arg);
}
}
return filtered;
}
/// Convert a [source] string to a Uri, where the source may be a
/// dart/file/package URI or a local win/mac/linux path.
///
/// If [source] is null, this will return null.
Uri sourcePathToUri(String source, {bool windows}) {
if (source == null) return null;
if (windows == null) {
// Running on the web the Platform check will fail, and we can't use
// fromEnvironment because internally it's set to true for dart.library.io.
// So just catch the exception and if it fails then we're definitely not on
// Windows.
try {
windows = Platform.isWindows;
} catch (e) {
windows = false;
}
}
if (windows) {
source = source.replaceAll("\\", "/");
}
Uri result = Uri.base.resolve(source);
if (windows && result.scheme.length == 1) {
// Assume c: or similar --- interpret as file path.
return Uri.file(source, windows: true);
}
return result;
}
Uri sourcePathToRelativeUri(String source, {bool windows}) {
var uri = sourcePathToUri(source, windows: windows);
if (uri.scheme == 'file') {
var uriPath = uri.path;
var root = Uri.base.path;
if (uriPath.startsWith(root)) {
return path.toUri(uriPath.substring(root.length));
}
}
return uri;
}
/// Adjusts the source paths in [sourceMap] to be relative to [sourceMapPath],
/// and returns the new map. Relative paths are in terms of URIs ('/'), not
/// local OS paths (e.g., windows '\'). Sources with a multi-root scheme
/// matching [multiRootScheme] are adjusted to be relative to
/// [multiRootOutputPath].
// TODO(jmesserly): find a new home for this.
Map placeSourceMap(Map sourceMap, String sourceMapPath,
Map<String, String> bazelMappings, String multiRootScheme,
{String multiRootOutputPath}) {
var map = Map.from(sourceMap);
// Convert to a local file path if it's not.
sourceMapPath = path.fromUri(sourcePathToUri(sourceMapPath));
var sourceMapDir = path.dirname(path.absolute(sourceMapPath));
var list = (map['sources'] as List).toList();
String makeRelative(String sourcePath) {
var uri = sourcePathToUri(sourcePath);
var scheme = uri.scheme;
if (scheme == 'dart' || scheme == 'package' || scheme == multiRootScheme) {
if (scheme == multiRootScheme) {
var multiRootPath = '$multiRootOutputPath${uri.path}';
multiRootPath = path.relative(multiRootPath, from: sourceMapDir);
return multiRootPath;
}
return sourcePath;
}
// Convert to a local file path if it's not.
sourcePath = path.absolute(path.fromUri(uri));
// Allow bazel mappings to override.
var match = bazelMappings[sourcePath];
if (match != null) return match;
// Fall back to a relative path against the source map itself.
sourcePath = path.relative(sourcePath, from: sourceMapDir);
// Convert from relative local path to relative URI.
return path.toUri(sourcePath).path;
}
for (int i = 0; i < list.length; i++) {
list[i] = makeRelative(list[i] as String);
}
map['sources'] = list;
map['file'] = makeRelative(map['file'] as String);
return map;
}
/// Invoke the compiler with [args], optionally with the kernel backend if
/// [isKernel] is set.
///
/// Returns a [CompilerResult], with a success flag indicating whether the
/// program compiled without any fatal errors.
///
/// The result may also contain a [previousResult], which can be passed back in
/// for batch/worker executions to attempt to existing state.
Future<CompilerResult> compile(ParsedArguments args,
{CompilerResult previousResult, Map<Uri, List<int>> inputDigests}) {
if (previousResult != null && !args.isBatchOrWorker) {
throw ArgumentError(
'previousResult requires --batch or --bazel_worker mode/');
}
if (args.isKernel) {
return kernel_compiler.compile(args.rest,
compilerState: previousResult?.kernelState,
useIncrementalCompiler: args.useIncrementalCompiler,
inputDigests: inputDigests);
} else {
var result = analyzer_compiler.compile(args.rest,
compilerState: previousResult?.analyzerState);
if (args.isBatchOrWorker) {
AnalysisEngine.instance.clearCaches();
}
return Future.value(result);
}
}
/// The result of a single `dartdevc` compilation.
///
/// Typically used for exiting the proceess with [exitCode] or checking the
/// [success] of the compilation.
///
/// For batch/worker compilations, the [compilerState] provides an opprotunity
/// to reuse state from the previous run, if the options/input summaries are
/// equiavlent. Otherwise it will be discarded.
class CompilerResult {
/// Optionally provides the front_end state from the previous compilation,
/// which can be passed to [compile] to potentially speeed up the next
/// compilation.
///
/// This field is unused when using the Analyzer-backend for DDC.
final InitializedCompilerState kernelState;
/// Optionally provides the analyzer state from the previous compilation,
/// which can be passed to [compile] to potentially speeed up the next
/// compilation.
///
/// This field is unused when using the Kernel-backend for DDC.
final CompilerAnalysisDriver analyzerState;
/// The process exit code of the compiler.
final int exitCode;
CompilerResult(this.exitCode, {this.kernelState, this.analyzerState}) {
assert(kernelState == null || analyzerState == null,
'kernel and analyzer state should not both be supplied');
}
/// Gets the kernel or analyzer compiler state, if any.
Object get compilerState => kernelState ?? analyzerState;
/// Whether the program compiled without any fatal errors (equivalent to
/// [exitCode] == 0).
bool get success => exitCode == 0;
/// Whether the compiler crashed (i.e. threw an unhandled exeception,
/// typically indicating an internal error in DDC itself or its front end).
bool get crashed => exitCode == 70;
}
/// Stores the result of preprocessing `dartdevc` command line arguments.
///
/// `dartdevc` preprocesses arguments to support some features that
/// `package:args` does not handle (training `@` to reference arguments in a
/// file).
///
/// [isBatch]/[isWorker] mode are preprocessed because they can combine
/// argument lists from the initial invocation and from batch/worker jobs.
///
/// [isKernel] is also preprocessed because the Kernel backend supports
/// different options compared to the Analyzer backend.
class ParsedArguments {
/// The user's arguments to the compiler for this compialtion.
final List<String> rest;
/// Whether to run in `--batch` mode, e.g the Dart SDK and Language tests.
///
/// Similar to [isWorker] but with a different protocol.
/// See also [isBatchOrWorker].
final bool isBatch;
/// Whether to run in `--bazel_worker` mode, e.g. for Bazel builds.
///
/// Similar to [isBatch] but with a different protocol.
/// See also [isBatchOrWorker].
final bool isWorker;
/// Whether to use the Kernel-based back end for dartdevc.
///
/// This is similar to the Analyzer-based back end, but uses Kernel trees
/// instead of Analyzer trees for representing the Dart code.
final bool isKernel;
/// Whether to re-use the last compiler result when in a worker.
///
/// This is useful if we are repeatedly compiling things in the same context,
/// e.g. in a debugger REPL.
final bool reuseResult;
/// Whether to use the incremental compiler for compiling.
///
/// Note that this only makes sense when also reusing results.
final bool useIncrementalCompiler;
ParsedArguments._(this.rest,
{this.isBatch = false,
this.isWorker = false,
this.isKernel = false,
this.reuseResult = false,
this.useIncrementalCompiler = false});
/// Preprocess arguments to determine whether DDK is used in batch mode or as a
/// persistent worker.
///
/// When used in batch mode, we expect a `--batch` parameter last.
///
/// When used as a persistent bazel worker, the `--persistent_worker` might be
/// present, and an argument of the form `@path/to/file` might be provided. The
/// latter needs to be replaced by reading all the contents of the
/// file and expanding them into the resulting argument list.
factory ParsedArguments.from(List<String> args) {
if (args.isEmpty) return ParsedArguments._(args);
var newArgs = <String>[];
bool isWorker = false;
bool isBatch = false;
bool isKernel = false;
bool reuseResult = false;
bool useIncrementalCompiler = false;
var len = args.length;
for (int i = 0; i < len; i++) {
var arg = args[i];
var isLastArg = i == len - 1;
if (isLastArg && arg.startsWith('@')) {
var extra = _readLines(arg.substring(1)).toList();
if (extra.remove('--kernel') || extra.remove('-k')) {
isKernel = true;
}
newArgs.addAll(extra);
} else if (arg == '--persistent_worker') {
isWorker = true;
} else if (isLastArg && arg == '--batch') {
isBatch = true;
} else if (arg == '--kernel' || arg == '-k') {
isKernel = true;
} else if (arg == '--reuse-compiler-result') {
reuseResult = true;
} else if (arg == '--use-incremental-compiler') {
useIncrementalCompiler = true;
} else {
newArgs.add(arg);
}
}
return ParsedArguments._(newArgs,
isWorker: isWorker,
isBatch: isBatch,
isKernel: isKernel,
reuseResult: reuseResult,
useIncrementalCompiler: useIncrementalCompiler);
}
/// Whether the compiler is running in [isBatch] or [isWorker] mode.
///
/// Both modes are generally equivalent from the compiler's perspective,
/// the main difference is that they use distinct protocols to communicate
/// jobs to the compiler.
bool get isBatchOrWorker => isBatch || isWorker;
/// Merge [args] and return the new parsed arguments.
///
/// Typically used when [isBatchOrWorker] is set to merge the compilation's
/// arguments with any global ones that were provided when the worker started.
ParsedArguments merge(List<String> arguments) {
// Parse the arguments again so `--kernel` can be passed. This provides
// added safety that we are really compiling in Kernel mode, if somehow the
// worker was not initialized correctly.
var newArgs = ParsedArguments.from(arguments);
if (newArgs.isBatchOrWorker) {
throw ArgumentError('cannot change batch or worker mode after startup.');
}
return ParsedArguments._(rest.toList()..addAll(newArgs.rest),
isWorker: isWorker,
isBatch: isBatch,
isKernel: isKernel || newArgs.isKernel,
reuseResult: reuseResult || newArgs.reuseResult,
useIncrementalCompiler:
useIncrementalCompiler || newArgs.useIncrementalCompiler);
}
}
/// Return all lines in a file found at [path].
Iterable<String> _readLines(String path) {
try {
return File(path).readAsLinesSync().where((String line) => line.isNotEmpty);
} on FileSystemException catch (e) {
throw Exception('Failed to read $path: $e');
}
}