#!/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 args) { // Parse flags. var parser = new 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 = new Stopwatch()..start(); var partial = new 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 = new File(outfile); var dartLibrary = new File(path.join(ddcPath, 'lib', 'js', 'legacy', 'dart_library.js')) .readAsStringSync(); out.writeAsStringSync(dartLibrary); var dartSdk = new 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 = new Future.value(); for (var module in orderedModules) { linearizerMap[module] = last; var completer = new Completer(); completerMap[module] = completer; last = completer.future; } // Build modules asynchronously var tmpdir = (tmp == null) ? Directory.systemTemp .createTempSync(outfile.replaceAll(path.separator, '__')) : new 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]) : []; 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 = new 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 = new Set(); final dependenceMap = new Map>(); final transitiveDependenceMap = new Map>(); final fileMap = new Map>(); final readyMap = new Map(); final linearizerMap = new Map(); final completerMap = new Map(); final orderedModules = new List(); final visitedModules = new Set(); void orderModules( [String module = ENTRY, List stack, Set visited]) { if (stack == null) { assert(visited == null); stack = new List(); visited = new Set(); } 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 = new Set(); 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, () => new Set()); 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, () => new Set()); 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 new 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)); } } } }