| // Copyright (c) 2017, 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:convert' show json; | |
| import 'dart:io'; | |
| import 'package:args/args.dart'; | |
| import 'package:build_integration/file_system/multi_root.dart'; | |
| import 'package:front_end/src/api_prototype/standard_file_system.dart'; | |
| import 'package:front_end/src/api_unstable/ddc.dart' as fe; | |
| import 'package:kernel/kernel.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:source_maps/source_maps.dart'; | |
| import '../compiler/js_names.dart' as JS; | |
| import '../compiler/module_builder.dart'; | |
| import '../compiler/shared_command.dart'; | |
| import '../js_ast/js_ast.dart' as JS; | |
| import '../js_ast/source_map_printer.dart' show SourceMapPrintingContext; | |
| import 'compiler.dart'; | |
| import 'target.dart'; | |
| const _binaryName = 'dartdevk'; | |
| /// Invoke the compiler with [args]. | |
| /// | |
| /// Returns `true` if the program compiled without any fatal errors. | |
| Future<CompilerResult> compile(List<String> args, | |
| {fe.InitializedCompilerState compilerState}) async { | |
| try { | |
| return await _compile(args, compilerState: compilerState); | |
| } catch (error, stackTrace) { | |
| print(''' | |
| We're sorry, you've found a bug in our compiler. | |
| You can report this bug at: | |
| https://github.com/dart-lang/sdk/issues/labels/area-dev-compiler | |
| Please include the information below in your report, along with | |
| any other information that may help us track it down. Thanks! | |
| -------------------- %< -------------------- | |
| $_binaryName arguments: ${args.join(' ')} | |
| dart --version: ${Platform.version} | |
| $error | |
| $stackTrace | |
| '''); | |
| rethrow; | |
| } | |
| } | |
| String _usageMessage(ArgParser ddcArgParser) => | |
| 'The Dart Development Compiler compiles Dart sources into a JavaScript ' | |
| 'module.\n\n' | |
| 'Usage: $_binaryName [options...] <sources...>\n\n' | |
| '${ddcArgParser.usage}'; | |
| /// Resolve [s] as a URI, possibly relative to the current directory. | |
| Uri stringToUri(String s, {bool windows}) { | |
| windows ??= Platform.isWindows; | |
| if (windows) { | |
| s = s.replaceAll("\\", "/"); | |
| } | |
| Uri result = Uri.base.resolve(s); | |
| if (windows && result.scheme.length == 1) { | |
| // Assume c: or similar --- interpret as file path. | |
| return new Uri.file(s, windows: true); | |
| } | |
| return result; | |
| } | |
| /// Resolve [s] as a URI, and if the URI is a uri under a directory in [roots], | |
| /// then return a custom URI containing only the subpath from that root and the | |
| /// provided [scheme]. For example, | |
| /// | |
| /// stringToCustomUri('a/b/c.dart', [Uri.base.resolve('a/')], 'foo') | |
| /// | |
| /// returns: | |
| /// | |
| /// foo:/b/c.dart | |
| /// | |
| /// This is used to create machine agnostic URIs both for input files and for | |
| /// summaries. We do so for input files to ensure we don't leak any | |
| /// user-specific paths into non-package library names, and we do so for input | |
| /// summaries to be able to easily derive a module name from the summary path. | |
| Uri stringToCustomUri(String s, List<Uri> roots, String scheme) { | |
| Uri resolvedUri = stringToUri(s); | |
| if (resolvedUri.scheme != 'file') return resolvedUri; | |
| for (var root in roots) { | |
| if (resolvedUri.path.startsWith(root.path)) { | |
| var path = resolvedUri.path.substring(root.path.length); | |
| return Uri.parse('$scheme:///$path'); | |
| } | |
| } | |
| return resolvedUri; | |
| } | |
| class CompilerResult { | |
| final fe.InitializedCompilerState compilerState; | |
| final bool result; | |
| CompilerResult(this.compilerState, this.result); | |
| CompilerResult.noState(this.result) : compilerState = null; | |
| } | |
| Future<CompilerResult> _compile(List<String> args, | |
| {fe.InitializedCompilerState compilerState}) async { | |
| // TODO(jmesserly): refactor options to share code with dartdevc CLI. | |
| var argParser = new ArgParser(allowTrailingOptions: true) | |
| ..addFlag('help', | |
| abbr: 'h', help: 'Display this message.', negatable: false) | |
| ..addOption('out', abbr: 'o', help: 'Output file (required).') | |
| ..addOption('packages', help: 'The package spec file to use.') | |
| // TODO(jmesserly): add verbose help to show hidden options | |
| ..addOption('dart-sdk-summary', | |
| help: 'The path to the Dart SDK summary file.', hide: true) | |
| ..addMultiOption('summary', | |
| abbr: 's', | |
| help: 'path to a summary of a transitive dependency of this module.\n' | |
| 'This path should be under a provided summary-input-dir') | |
| ..addFlag('source-map', help: 'emit source mapping', defaultsTo: true) | |
| ..addMultiOption('summary-input-dir') | |
| ..addOption('custom-app-scheme', defaultsTo: 'org-dartlang-app') | |
| ..addFlag('emit-metadata', | |
| help: '(deprecated) enables dart:mirrors for this module', hide: true) | |
| ..addFlag('enable-asserts', help: 'enable assertions', defaultsTo: true) | |
| // Ignore dart2js options that we don't support in DDC. | |
| // TODO(jmesserly): add ignore-unrecognized-flag support. | |
| ..addFlag('enable-enum', hide: true) | |
| ..addFlag('experimental-trust-js-interop-type-annotations', hide: true) | |
| ..addFlag('trust-type-annotations', hide: true) | |
| ..addFlag('supermixin', hide: true); | |
| addModuleFormatOptions(argParser, singleOutFile: false); | |
| var declaredVariables = parseAndRemoveDeclaredVariables(args); | |
| var argResults = argParser.parse(args); | |
| if (argResults['help'] as bool || args.isEmpty) { | |
| print(_usageMessage(argParser)); | |
| return new CompilerResult.noState(true); | |
| } | |
| var moduleFormat = parseModuleFormatOption(argResults).first; | |
| var ddcPath = path.dirname(path.dirname(path.fromUri(Platform.script))); | |
| var multiRoots = <Uri>[]; | |
| for (var s in argResults['summary-input-dir'] as List<String>) { | |
| var uri = stringToUri(s); | |
| if (!uri.path.endsWith('/')) { | |
| uri = uri.replace(path: '${uri.path}/'); | |
| } | |
| multiRoots.add(uri); | |
| } | |
| multiRoots.add(Uri.base); | |
| var customScheme = argResults['custom-app-scheme'] as String; | |
| var summaryUris = (argResults['summary'] as List<String>) | |
| .map((s) => stringToCustomUri(s, multiRoots, customScheme)) | |
| .toList(); | |
| var sdkSummaryPath = | |
| argResults['dart-sdk-summary'] as String ?? defaultSdkSummaryPath; | |
| var packageFile = argResults['packages'] as String ?? | |
| path.absolute(ddcPath, '..', '..', '.packages'); | |
| var inputs = argResults.rest | |
| .map((s) => stringToCustomUri(s, [Uri.base], customScheme)) | |
| .toList(); | |
| var succeeded = true; | |
| void errorHandler(fe.CompilationMessage error) { | |
| if (error.severity == fe.Severity.error) { | |
| succeeded = false; | |
| } | |
| } | |
| // To make the output .dill agnostic of the current working directory, | |
| // we use a custom-uri scheme for all app URIs (these are files outside the | |
| // lib folder). The following [FileSystem] will resolve those references to | |
| // the correct location and keeps the real file location hidden from the | |
| // front end. | |
| var fileSystem = new MultiRootFileSystem( | |
| customScheme, multiRoots, StandardFileSystem.instance); | |
| compilerState = await fe.initializeCompiler( | |
| compilerState, | |
| stringToUri(sdkSummaryPath), | |
| stringToUri(packageFile), | |
| summaryUris, | |
| new DevCompilerTarget(), | |
| fileSystem: fileSystem); | |
| fe.DdcResult result = await fe.compile(compilerState, inputs, errorHandler); | |
| if (result == null || !succeeded) { | |
| return new CompilerResult(compilerState, false); | |
| } | |
| var component = result.component; | |
| var emitMetadata = argResults['emit-metadata'] as bool; | |
| if (!emitMetadata && _checkForDartMirrorsImport(component)) { | |
| return new CompilerResult(compilerState, false); | |
| } | |
| String output = argResults['out']; | |
| var file = new File(output); | |
| if (!file.parent.existsSync()) file.parent.createSync(recursive: true); | |
| // TODO(jmesserly): Save .dill file so other modules can link in this one. | |
| //await writeComponentToBinary(component, output); | |
| // Useful for debugging: | |
| writeComponentToText(component, path: output + '.txt'); | |
| var compiler = new ProgramCompiler(component, | |
| declaredVariables: declaredVariables, | |
| emitMetadata: emitMetadata, | |
| enableAsserts: argResults['enable-asserts'] as bool); | |
| var jsModule = | |
| compiler.emitModule(component, result.inputSummaries, summaryUris); | |
| var jsCode = jsProgramToCode(jsModule, moduleFormat, | |
| buildSourceMap: argResults['source-map'] as bool, | |
| jsUrl: path.toUri(output).toString(), | |
| mapUrl: path.toUri(output + '.map').toString(), | |
| customScheme: customScheme); | |
| file.writeAsStringSync(jsCode.code); | |
| if (jsCode.sourceMap != null) { | |
| file = new File(output + '.map'); | |
| if (!file.parent.existsSync()) file.parent.createSync(recursive: true); | |
| file.writeAsStringSync(json.encode(jsCode.sourceMap)); | |
| } | |
| return new CompilerResult(compilerState, true); | |
| } | |
| /// The output of compiling a JavaScript module in a particular format. | |
| /// This was copied from module_compiler.dart class "JSModuleCode". | |
| class JSCode { | |
| /// The JavaScript code for this module. | |
| /// | |
| /// If a [sourceMap] is available, this will include the `sourceMappingURL` | |
| /// comment at end of the file. | |
| final String code; | |
| /// The JSON of the source map, if generated, otherwise `null`. | |
| /// | |
| /// The source paths will initially be absolute paths. They can be adjusted | |
| /// using [placeSourceMap]. | |
| final Map sourceMap; | |
| JSCode(this.code, this.sourceMap); | |
| } | |
| JSCode jsProgramToCode(JS.Program moduleTree, ModuleFormat format, | |
| {bool buildSourceMap: false, | |
| String jsUrl, | |
| String mapUrl, | |
| String customScheme}) { | |
| var opts = new JS.JavaScriptPrintingOptions( | |
| allowKeywordsInProperties: true, allowSingleLineIfStatements: true); | |
| JS.SimpleJavaScriptPrintingContext printer; | |
| SourceMapBuilder sourceMap; | |
| if (buildSourceMap) { | |
| var sourceMapContext = new SourceMapPrintingContext(); | |
| sourceMap = sourceMapContext.sourceMap; | |
| printer = sourceMapContext; | |
| } else { | |
| printer = new JS.SimpleJavaScriptPrintingContext(); | |
| } | |
| var tree = transformModuleFormat(format, moduleTree); | |
| tree.accept( | |
| new JS.Printer(opts, printer, localNamer: new JS.TemporaryNamer(tree))); | |
| Map builtMap; | |
| if (buildSourceMap && sourceMap != null) { | |
| builtMap = placeSourceMap( | |
| sourceMap.build(jsUrl), mapUrl, <String, String>{}, customScheme); | |
| var jsDir = path.dirname(path.fromUri(jsUrl)); | |
| var relative = path.relative(path.fromUri(mapUrl), from: jsDir); | |
| var relativeMapUrl = path.toUri(relative).toString(); | |
| assert(path.dirname(jsUrl) == path.dirname(mapUrl)); | |
| printer.emit('\n//# sourceMappingURL='); | |
| printer.emit(relativeMapUrl); | |
| printer.emit('\n'); | |
| } | |
| var text = printer.getText(); | |
| return new JSCode(text, builtMap); | |
| } | |
| /// This was copied from module_compiler.dart. | |
| /// 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. | |
| // TODO(sigmund): delete bazelMappings - customScheme should be used instead. | |
| Map placeSourceMap(Map sourceMap, String sourceMapPath, | |
| Map<String, String> bazelMappings, String customScheme) { | |
| var map = new Map.from(sourceMap); | |
| // Convert to a local file path if it's not. | |
| sourceMapPath = path.fromUri(_sourceToUri(sourceMapPath, customScheme)); | |
| var sourceMapDir = path.dirname(path.absolute(sourceMapPath)); | |
| var list = (map['sources'] as List).toList(); | |
| map['sources'] = list; | |
| String makeRelative(String sourcePath) { | |
| var uri = _sourceToUri(sourcePath, customScheme); | |
| if (uri.scheme == 'dart' || | |
| uri.scheme == 'package' || | |
| uri.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; | |
| } | |
| /// This was copied from module_compiler.dart. | |
| /// Convert a source string to a Uri. The [source] may be a Dart URI, a file | |
| /// URI, or a local win/mac/linux path. | |
| Uri _sourceToUri(String source, customScheme) { | |
| var uri = Uri.parse(source); | |
| var scheme = uri.scheme; | |
| if (scheme == "dart" || | |
| scheme == "package" || | |
| scheme == "file" || | |
| scheme == customScheme) { | |
| // A valid URI. | |
| return uri; | |
| } | |
| // Assume a file path. | |
| // TODO(jmesserly): shouldn't this be `path.toUri(path.absolute)`? | |
| return new Uri.file(path.absolute(source)); | |
| } | |
| /// Parses Dart's non-standard `-Dname=value` syntax for declared variables, | |
| /// and removes them from [args] so the result can be parsed normally. | |
| Map<String, String> parseAndRemoveDeclaredVariables(List<String> args) { | |
| var declaredVariables = <String, String>{}; | |
| for (int i = 0; i < args.length;) { | |
| var arg = args[i]; | |
| if (arg.startsWith('-D') && arg.length > 2) { | |
| var rest = arg.substring(2); | |
| var eq = rest.indexOf('='); | |
| if (eq <= 0) { | |
| var kind = eq == 0 ? 'name' : 'value'; | |
| throw new FormatException('no $kind given to -D option `$arg`'); | |
| } | |
| var name = rest.substring(0, eq); | |
| var value = rest.substring(eq + 1); | |
| declaredVariables[name] = value; | |
| args.removeAt(i); | |
| } else { | |
| i++; | |
| } | |
| } | |
| // Add platform defined variables | |
| declaredVariables.addAll(sdkLibraryVariables); | |
| return declaredVariables; | |
| } | |
| /// The default path of the kernel summary for the Dart SDK. | |
| final defaultSdkSummaryPath = path.join( | |
| path.dirname(path.dirname(Platform.resolvedExecutable)), | |
| 'lib', | |
| '_internal', | |
| 'ddc_sdk.dill'); | |
| bool _checkForDartMirrorsImport(Component component) { | |
| for (var library in component.libraries) { | |
| if (library.isExternal) continue; | |
| for (var dep in library.dependencies) { | |
| var uri = dep.targetLibrary.importUri; | |
| if (uri.scheme == 'dart' && uri.path == 'mirrors') { | |
| print('${library.importUri}: Error: Cannot import "dart:mirrors" ' | |
| 'in web applications (https://goo.gl/R1anEs).'); | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } |