| #!/usr/bin/env dart |
| // Copyright (c) 2016, 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:io'; |
| |
| import 'package:analyzer/analyzer.dart' show StringLiteral, parseDirectives; |
| import 'package:analyzer/src/dart/ast/ast.dart'; |
| import 'package:args/args.dart' show ArgParser; |
| import 'package:path/path.dart' as path; |
| |
| const ENTRY = "main"; |
| |
| void main(List<String> args) { |
| // Parse flags. |
| var parser = ArgParser() |
| ..addOption('out', |
| help: 'Output file (defaults to "out.js")', |
| abbr: 'o', |
| defaultsTo: 'out.js') |
| ..addFlag('unsafe-force-compile', |
| help: 'Generate code with undefined behavior', negatable: false) |
| ..addFlag('emit-metadata', |
| help: 'Preserve annotations in generated code', negatable: false) |
| ..addOption('package-root', |
| help: 'Directory containing packages', |
| abbr: 'p', |
| defaultsTo: 'packages/') |
| ..addFlag('log', help: 'Show individual build commands') |
| ..addOption('tmp', |
| help: |
| 'Directory for temporary artifacts (defaults to a system tmp directory)'); |
| |
| var options = parser.parse(args); |
| if (options.rest.length != 1) { |
| throw 'Expected a single dart entrypoint.'; |
| } |
| var entry = options.rest.first; |
| var outfile = options['out'] as String; |
| var packageRoot = options['package-root'] as String; |
| var unsafe = options['unsafe-force-compile'] as bool; |
| var log = options['log'] as bool; |
| var tmp = options['tmp'] as String; |
| var metadata = options['emit-metadata'] as bool; |
| |
| // Build an invocation to dartdevc |
| var dartPath = Platform.resolvedExecutable; |
| var ddcPath = path.dirname(path.dirname(Platform.script.toFilePath())); |
| var template = [ |
| '$ddcPath/bin/dartdevc.dart', |
| '--modules=legacy', // TODO(vsm): Change this to use common format. |
| '--single-out-file', |
| '--inline-source-map', |
| '-p', |
| packageRoot |
| ]; |
| if (metadata) { |
| template.add('--emit-metadata'); |
| } |
| if (unsafe) { |
| template.add('--unsafe-force-compile'); |
| } |
| |
| // Compute the transitive closure |
| var total = Stopwatch()..start(); |
| var partial = Stopwatch()..start(); |
| |
| // TODO(vsm): We're using the analyzer just to compute the import/export/part |
| // dependence graph. This is expensive. Is there a lighterweight way to do |
| // this? |
| transitiveFiles(entry, Directory.current.path, packageRoot); |
| orderModules(); |
| computeTransitiveDependences(); |
| |
| var graphTime = partial.elapsedMilliseconds / 1000; |
| print('Computed global build graph in $graphTime seconds'); |
| |
| // Prepend Dart runtime files to the output |
| var out = File(outfile); |
| var dartLibrary = |
| File(path.join(ddcPath, 'lib', 'js', 'legacy', 'dart_library.js')) |
| .readAsStringSync(); |
| out.writeAsStringSync(dartLibrary); |
| var dartSdk = File(path.join(ddcPath, 'lib', 'js', 'legacy', 'dart_sdk.js')) |
| .readAsStringSync(); |
| out.writeAsStringSync(dartSdk, mode: FileMode.append); |
| |
| // Linearize module concatenation for deterministic output |
| var last = Future.value(); |
| for (var module in orderedModules) { |
| linearizerMap[module] = last; |
| var completer = Completer(); |
| completerMap[module] = completer; |
| last = completer.future; |
| } |
| |
| // Build modules asynchronously |
| var tmpdir = (tmp == null) |
| ? Directory.systemTemp |
| .createTempSync(outfile.replaceAll(path.separator, '__')) |
| : Directory(tmp) |
| ..createSync(); |
| for (var module in orderedModules) { |
| var file = tmpdir.path + path.separator + module + '.js'; |
| var command = template.toList()..addAll(['-o', file]); |
| var dependences = transitiveDependenceMap[module]; |
| for (var dependence in dependences) { |
| var summary = tmpdir.path + path.separator + dependence + '.sum'; |
| command.addAll(['-s', summary]); |
| } |
| var infiles = fileMap[module]; |
| command.addAll(infiles); |
| |
| var waitList = dependenceMap.containsKey(module) |
| ? dependenceMap[module].map((dep) => readyMap[dep]) |
| : <Future>[]; |
| var future = Future.wait(waitList); |
| readyMap[module] = future.then((_) { |
| var ready = Process.run(dartPath, command); |
| if (log) { |
| print(command.join(' ')); |
| } |
| return ready.then((result) { |
| if (result.exitCode != 0) { |
| print('ERROR: compiling $module'); |
| print(result.stdout); |
| print(result.stderr); |
| out.deleteSync(); |
| exit(1); |
| } |
| print('Compiled $module (${infiles.length} files)'); |
| print(result.stdout); |
| |
| // Schedule module append once the previous module is written |
| var codefile = File(file); |
| linearizerMap[module] |
| .then((_) => codefile.readAsString()) |
| .then((code) => |
| out.writeAsString(code, mode: FileMode.append, flush: true)) |
| .then((_) => completerMap[module].complete()); |
| }); |
| }); |
| } |
| |
| last.then((_) { |
| var time = total.elapsedMilliseconds / 1000; |
| print('Successfully compiled ${inputSet.length} files in $time seconds'); |
| |
| // Append the entry point invocation. |
| var libraryName = |
| path.withoutExtension(entry).replaceAll(path.separator, '__'); |
| out.writeAsStringSync('dart_library.start("$ENTRY", "$libraryName");\n', |
| mode: FileMode.append); |
| }); |
| } |
| |
| final inputSet = Set<String>(); |
| final dependenceMap = Map<String, Set<String>>(); |
| final transitiveDependenceMap = Map<String, Set<String>>(); |
| final fileMap = Map<String, Set<String>>(); |
| |
| final readyMap = Map<String, Future>(); |
| final linearizerMap = Map<String, Future>(); |
| final completerMap = Map<String, Completer>(); |
| |
| final orderedModules = List<String>(); |
| final visitedModules = Set<String>(); |
| |
| void orderModules( |
| [String module = ENTRY, List<String> stack, Set<String> visited]) { |
| if (stack == null) { |
| assert(visited == null); |
| stack = List<String>(); |
| visited = Set<String>(); |
| } |
| if (visited.contains(module)) return; |
| visited.add(module); |
| if (stack.contains(module)) { |
| print(stack); |
| throw 'Circular dependence on $module'; |
| } |
| stack.add(module); |
| var dependences = dependenceMap[module]; |
| if (dependences != null) { |
| for (var dependence in dependences) { |
| orderModules(dependence, stack, visited); |
| } |
| } |
| orderedModules.add(module); |
| assert(module == stack.last); |
| stack.removeLast(); |
| } |
| |
| void computeTransitiveDependences() { |
| for (var module in orderedModules) { |
| var transitiveSet = Set<String>(); |
| if (dependenceMap.containsKey(module)) { |
| transitiveSet.addAll(dependenceMap[module]); |
| for (var dependence in dependenceMap[module]) { |
| transitiveSet.addAll(transitiveDependenceMap[dependence]); |
| } |
| } |
| transitiveDependenceMap[module] = transitiveSet; |
| } |
| } |
| |
| String getModule(String uri) { |
| var sourceUri = Uri.parse(uri); |
| if (sourceUri.scheme == 'dart') { |
| return 'dart'; |
| } else if (sourceUri.scheme == 'package') { |
| return path.split(sourceUri.path)[0]; |
| } else { |
| return ENTRY; |
| } |
| } |
| |
| bool processFile(String file) { |
| inputSet.add(file); |
| |
| var module = getModule(file); |
| fileMap.putIfAbsent(module, () => Set<String>()); |
| return fileMap[module].add(file); |
| } |
| |
| void processDependence(String from, String to) { |
| var fromModule = getModule(from); |
| var toModule = getModule(to); |
| if (fromModule == toModule || toModule == 'dart') return; |
| dependenceMap.putIfAbsent(fromModule, () => Set<String>()); |
| dependenceMap[fromModule].add(toModule); |
| } |
| |
| String canonicalize(String uri, String root) { |
| var sourceUri = Uri.parse(uri); |
| if (sourceUri.scheme == '') { |
| sourceUri = path.toUri( |
| path.isAbsolute(uri) ? path.absolute(uri) : path.join(root, uri)); |
| return sourceUri.path; |
| } |
| return sourceUri.toString(); |
| } |
| |
| /// Simplified from ParseDartTask.resolveDirective. |
| String _resolveDirective(UriBasedDirectiveImpl directive) { |
| StringLiteral uriLiteral = directive.uri; |
| String uriContent = uriLiteral.stringValue; |
| if (uriContent != null) { |
| uriContent = uriContent.trim(); |
| directive.uriContent = uriContent; |
| } |
| return directive.validate() == null ? uriContent : null; |
| } |
| |
| String _loadFile(String uri, String packageRoot) { |
| if (uri.startsWith('package:')) { |
| uri = path.join(packageRoot, uri.substring(8)); |
| } |
| return File(uri).readAsStringSync(); |
| } |
| |
| void transitiveFiles(String entryPoint, String root, String packageRoot) { |
| entryPoint = canonicalize(entryPoint, root); |
| if (entryPoint.startsWith('dart:')) return; |
| if (processFile(entryPoint)) { |
| // Process this |
| var source = _loadFile(entryPoint, packageRoot); |
| var entryDir = path.dirname(entryPoint); |
| var unit = parseDirectives(source, name: entryPoint, suppressErrors: true); |
| for (var d in unit.directives) { |
| if (d is NamespaceDirectiveImpl) { |
| var uri = _resolveDirective(d); |
| processDependence(entryPoint, canonicalize(uri, entryDir)); |
| transitiveFiles(uri, entryDir, packageRoot); |
| } else if (d is PartDirectiveImpl) { |
| var uri = _resolveDirective(d); |
| processFile(canonicalize(uri, entryDir)); |
| } |
| } |
| } |
| } |