313 lines
12 KiB
Dart
313 lines
12 KiB
Dart
// Copyright (c) 2015, 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.
|
|
|
|
/// Command line tool to run the checker on a Dart program.
|
|
library dev_compiler.devc;
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:analyzer/src/generated/engine.dart' show ChangeSet;
|
|
import 'package:logging/logging.dart' show Level, Logger, LogRecord;
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:shelf/shelf.dart' as shelf;
|
|
import 'package:shelf/shelf_io.dart' as shelf;
|
|
import 'package:shelf_static/shelf_static.dart' as shelf_static;
|
|
|
|
import 'src/checker/checker.dart';
|
|
import 'src/checker/dart_sdk.dart' show mockSdkSources;
|
|
import 'src/checker/resolver.dart';
|
|
import 'src/checker/rules.dart';
|
|
import 'src/codegen/code_generator.dart' show CodeGenerator;
|
|
import 'src/codegen/dart_codegen.dart';
|
|
import 'src/codegen/html_codegen.dart';
|
|
import 'src/codegen/js_codegen.dart';
|
|
import 'src/dependency_graph.dart';
|
|
import 'src/info.dart' show LibraryInfo, CheckerResults, LibraryUnit;
|
|
import 'src/options.dart';
|
|
import 'src/report.dart';
|
|
import 'src/utils.dart';
|
|
|
|
/// Sets up the type checker logger to print a span that highlights error
|
|
/// messages.
|
|
StreamSubscription setupLogger(Level level, printFn) {
|
|
Logger.root.level = level;
|
|
return Logger.root.onRecord.listen((LogRecord rec) {
|
|
printFn('${rec.level.name.toLowerCase()}: ${rec.message}');
|
|
});
|
|
}
|
|
|
|
/// Encapsulates the logic to do a one-off compilation or a partial compilation
|
|
/// when the compiler is run as a development server.
|
|
class Compiler {
|
|
final CompilerOptions _options;
|
|
final TypeResolver _resolver;
|
|
final CheckerReporter _reporter;
|
|
final TypeRules _rules;
|
|
final CodeChecker _checker;
|
|
final SourceGraph _graph;
|
|
final SourceNode _entryNode;
|
|
List<LibraryInfo> _libraries = <LibraryInfo>[];
|
|
final List<CodeGenerator> _generators;
|
|
final bool _hashing;
|
|
bool _failure = false;
|
|
|
|
factory Compiler(CompilerOptions options,
|
|
[TypeResolver resolver, CheckerReporter reporter]) {
|
|
if (resolver == null) {
|
|
resolver = options.useMockSdk
|
|
? new TypeResolver.fromMock(mockSdkSources, options)
|
|
: new TypeResolver.fromDir(options.dartSdkPath, options);
|
|
}
|
|
|
|
if (reporter == null) {
|
|
reporter = options.dumpInfo
|
|
? new SummaryReporter()
|
|
: new LogReporter(options.useColors);
|
|
}
|
|
var graph = new SourceGraph(resolver.context, reporter, options);
|
|
var rules = new RestrictedRules(resolver.context.typeProvider, reporter,
|
|
options: options);
|
|
var checker = new CodeChecker(rules, reporter, options);
|
|
var inputFile = options.entryPointFile;
|
|
var uri = inputFile.startsWith('dart:') || inputFile.startsWith('package:')
|
|
? Uri.parse(inputFile)
|
|
: new Uri.file(path.absolute(inputFile));
|
|
var entryNode = graph.nodeFromUri(uri);
|
|
|
|
var outputDir = options.outputDir;
|
|
var generators = <CodeGenerator>[];
|
|
if (options.dumpSrcDir != null) {
|
|
generators.add(new EmptyDartGenerator(
|
|
options.dumpSrcDir, entryNode.uri, rules, options));
|
|
}
|
|
if (outputDir != null) {
|
|
generators.add(options.outputDart
|
|
? new DartGenerator(outputDir, entryNode.uri, rules, options)
|
|
: new JSGenerator(outputDir, entryNode.uri, rules, options));
|
|
}
|
|
return new Compiler._(options, resolver, reporter, rules, checker, graph,
|
|
entryNode, generators,
|
|
// TODO(sigmund): refactor to support hashing of the dart output?
|
|
options.serverMode && generators.length == 1 && !options.outputDart);
|
|
}
|
|
|
|
Compiler._(this._options, this._resolver, this._reporter, this._rules,
|
|
this._checker, this._graph, this._entryNode, this._generators,
|
|
this._hashing);
|
|
|
|
bool _buildSource(SourceNode node) {
|
|
if (node is HtmlSourceNode) {
|
|
_buildHtmlFile(node);
|
|
} else if (node is DartSourceNode) {
|
|
_buildDartLibrary(node);
|
|
} else if (node is ResourceSourceNode) {
|
|
_buildResourceFile(node);
|
|
} else {
|
|
assert(false); // should not get a build request on PartSourceNode
|
|
}
|
|
|
|
// TODO(sigmund): don't always return true. Use summarization to better
|
|
// determine when rebuilding is needed.
|
|
return true;
|
|
}
|
|
|
|
void _buildHtmlFile(HtmlSourceNode node) {
|
|
if (_options.outputDir == null) return;
|
|
var uri = node.source.uri;
|
|
_reporter.enterHtml(uri);
|
|
var output = generateEntryHtml(node, _options);
|
|
if (output == null) {
|
|
_failure = true;
|
|
return;
|
|
}
|
|
_reporter.leaveHtml();
|
|
var filename = path.basename(node.uri.path);
|
|
String outputFile = path.join(_options.outputDir, filename);
|
|
new File(outputFile).writeAsStringSync(output);
|
|
|
|
if (_options.outputDart) return;
|
|
}
|
|
|
|
void _buildResourceFile(ResourceSourceNode node) {
|
|
// ResourceSourceNodes files that just need to be copied over to the output
|
|
// location. These can be external dependencies or pieces of the
|
|
// dev_compiler runtime.
|
|
if (_options.outputDir == null || _options.outputDart) return;
|
|
var filepath = resourceOutputPath(node.uri, _entryNode.uri);
|
|
assert(filepath != null);
|
|
filepath = path.join(_options.outputDir, filepath);
|
|
var dir = path.dirname(filepath);
|
|
new Directory(dir).createSync(recursive: true);
|
|
new File.fromUri(node.source.uri).copySync(filepath);
|
|
if (_hashing) node.cachingHash = computeHashFromFile(filepath);
|
|
}
|
|
|
|
bool _isEntry(DartSourceNode node) {
|
|
if (_entryNode is DartSourceNode) return _entryNode == node;
|
|
return (_entryNode as HtmlSourceNode).scripts.contains(node);
|
|
}
|
|
|
|
void _buildDartLibrary(DartSourceNode node) {
|
|
var source = node.source;
|
|
// TODO(sigmund): find out from analyzer team if there is a better way
|
|
_resolver.context.applyChanges(new ChangeSet()..changedSource(source));
|
|
var entryUnit = _resolver.context.resolveCompilationUnit2(source, source);
|
|
var lib = entryUnit.element.enclosingElement;
|
|
if (!_options.checkSdk && lib.isInSdk) return;
|
|
var current = node.info;
|
|
if (current != null) {
|
|
assert(current.library == lib);
|
|
} else {
|
|
node.info = current = new LibraryInfo(lib, _isEntry(node));
|
|
}
|
|
_reporter.enterLibrary(source.uri);
|
|
_libraries.add(current);
|
|
_rules.currentLibraryInfo = current;
|
|
|
|
var resolvedParts = node.parts
|
|
.map((p) => _resolver.context.resolveCompilationUnit2(p.source, source))
|
|
.toList(growable: false);
|
|
var libraryUnit = new LibraryUnit(entryUnit, resolvedParts);
|
|
bool failureInLib = false;
|
|
for (var unit in libraryUnit.libraryThenParts) {
|
|
var unitSource = unit.element.source;
|
|
_reporter.enterSource(unitSource);
|
|
// TODO(sigmund): integrate analyzer errors with static-info (issue #6).
|
|
failureInLib = _resolver.logErrors(unitSource, _reporter) || failureInLib;
|
|
unit.visitChildren(_checker);
|
|
if (_checker.failure) failureInLib = true;
|
|
_reporter.leaveSource();
|
|
}
|
|
if (failureInLib) {
|
|
_failure = true;
|
|
if (!_options.forceCompile) return;
|
|
}
|
|
|
|
for (var cg in _generators) {
|
|
var hash = cg.generateLibrary(libraryUnit, current, _reporter);
|
|
if (_hashing) node.cachingHash = hash;
|
|
}
|
|
_reporter.leaveLibrary();
|
|
}
|
|
|
|
CheckerResults run() {
|
|
var clock = new Stopwatch()..start();
|
|
|
|
// TODO(sigmund): we are missing a couple failures here. The
|
|
// dependency_graph now detects broken imports or unsupported features
|
|
// like more than one script tag (see .severe messages in
|
|
// dependency_graph.dart). Such failures should be reported back
|
|
// here so we can mark failure=true in the CheckerResutls.
|
|
rebuild(_entryNode, _graph, _buildSource);
|
|
_dumpInfoIfRequested();
|
|
clock.stop();
|
|
var time = (clock.elapsedMilliseconds / 1000).toStringAsFixed(2);
|
|
_log.fine('Compiled ${_libraries.length} libraries in ${time} s\n');
|
|
return new CheckerResults(
|
|
_libraries, _rules, _failure || _options.forceCompile);
|
|
}
|
|
|
|
void _runAgain() {
|
|
var clock = new Stopwatch()..start();
|
|
_libraries = <LibraryInfo>[];
|
|
int changed = 0;
|
|
|
|
// TODO(sigmund): propagate failures here (see TODO in run).
|
|
rebuild(_entryNode, _graph, (n) {
|
|
changed++;
|
|
return _buildSource(n);
|
|
});
|
|
clock.stop();
|
|
if (changed > 0) _dumpInfoIfRequested();
|
|
var time = (clock.elapsedMilliseconds / 1000).toStringAsFixed(2);
|
|
_log.fine("Compiled ${changed} libraries in ${time} s\n");
|
|
}
|
|
|
|
_dumpInfoIfRequested() {
|
|
if (!_options.dumpInfo || _reporter is! SummaryReporter) return;
|
|
var result = (_reporter as SummaryReporter).result;
|
|
if (!_options.serverMode) print(summaryToString(result));
|
|
var filepath = _options.serverMode
|
|
? path.join(_options.outputDir, 'messages.json')
|
|
: _options.dumpInfoFile;
|
|
if (filepath == null) return;
|
|
new File(filepath).writeAsStringSync(JSON.encode(result.toJsonMap()));
|
|
}
|
|
}
|
|
|
|
class CompilerServer {
|
|
final Compiler compiler;
|
|
final String outDir;
|
|
final int port;
|
|
final String _entryPath;
|
|
|
|
factory CompilerServer(CompilerOptions options) {
|
|
var entryPath = path.basename(options.entryPointFile);
|
|
if (path.extension(entryPath) != '.html') {
|
|
print('error: devc in server mode requires an HTML entry point.');
|
|
exit(1);
|
|
}
|
|
|
|
// TODO(sigmund): allow running without a dir, but keep output in memory?
|
|
var outDir = options.outputDir;
|
|
if (outDir == null) {
|
|
print('error: devc in server mode also requires specifying and '
|
|
'output location for generated code.');
|
|
exit(1);
|
|
}
|
|
var port = options.port;
|
|
_log.fine('Serving $entryPath at http://0.0.0.0:$port/');
|
|
var compiler = new Compiler(options);
|
|
return new CompilerServer._(compiler, outDir, port, entryPath);
|
|
}
|
|
|
|
CompilerServer._(this.compiler, this.outDir, this.port, this._entryPath);
|
|
|
|
Future start() async {
|
|
// Create output directory if needed. shelf_static will fail otherwise.
|
|
var out = new Directory(outDir);
|
|
if (!await out.exists()) await out.create(recursive: true);
|
|
|
|
var handler = const shelf.Pipeline()
|
|
.addMiddleware(rebuildAndCache)
|
|
.addHandler(shelf_static.createStaticHandler(outDir,
|
|
defaultDocument: _entryPath));
|
|
await shelf.serve(handler, '0.0.0.0', port);
|
|
compiler.run();
|
|
}
|
|
|
|
shelf.Handler rebuildAndCache(shelf.Handler handler) => (request) {
|
|
_log.fine('requested $GREEN_COLOR${request.url}$NO_COLOR');
|
|
// Trigger recompile only when requesting the HTML page.
|
|
var segments = request.url.pathSegments;
|
|
bool isEntryPage = segments.length == 0 || segments[0] == _entryPath;
|
|
if (isEntryPage) compiler._runAgain();
|
|
|
|
// To help browsers cache resources that don't change, we serve these
|
|
// resources under a path containing their hash:
|
|
// /cached/{hash}/{path-to-file.js}
|
|
bool isCached = segments.length > 1 && segments[0] == 'cached';
|
|
if (isCached) {
|
|
// Changing the request lets us record that the hash prefix is handled
|
|
// here, and that the underlying handler should use the rest of the url to
|
|
// determine where to find the resource in the file system.
|
|
request = request.change(path: path.join('cached', segments[1]));
|
|
}
|
|
var response = handler(request);
|
|
var policy = isCached ? 'max-age=${24 * 60 * 60}' : 'no-cache';
|
|
var headers = {'cache-control': policy};
|
|
if (isCached) {
|
|
// Note: the cache-control header should be enough, but this doesn't hurt
|
|
// and can help renew the policy after it expires.
|
|
headers['ETag'] = segments[1];
|
|
}
|
|
return response.change(headers: headers);
|
|
};
|
|
}
|
|
|
|
final _log = new Logger('dev_compiler');
|
|
final _earlyErrorResult = new CheckerResults(const [], null, true);
|