| // 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 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.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, |
| 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)); |
| |
| 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('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); |
| } |
| |
| 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 '\'). |
| // TODO(jmesserly): find a new home for this. |
| Map placeSourceMap(Map sourceMap, String sourceMapPath, |
| Map<String, String> bazelMappings, String customScheme) { |
| 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(); |
| map['sources'] = list; |
| |
| String makeRelative(String sourcePath) { |
| var uri = sourcePathToUri(sourcePath); |
| var scheme = uri.scheme; |
| if (scheme == 'dart' || scheme == 'package' || scheme == customScheme) { |
| 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['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}) { |
| 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); |
| } 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; |
| |
| ParsedArguments._(this.rest, |
| {this.isBatch = false, |
| this.isWorker = false, |
| this.isKernel = false, |
| this.reuseResult = 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; |
| 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 { |
| newArgs.add(arg); |
| } |
| } |
| return ParsedArguments._(newArgs, |
| isWorker: isWorker, |
| isBatch: isBatch, |
| isKernel: isKernel, |
| reuseResult: reuseResult); |
| } |
| |
| /// 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); |
| } |
| } |
| |
| /// 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'); |
| } |
| } |