2bb32ec55a
Change-Id: Ib1afd38a7d3694c2e5ed591132f41b147f76d75a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427560 Reviewed-by: Stephen Adams <sra@google.com> Commit-Queue: Mayank Patke <fishythefish@google.com>
627 lines
18 KiB
Dart
627 lines
18 KiB
Dart
// Copyright (c) 2013, 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.
|
|
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
// ignore: implementation_imports
|
|
import 'package:front_end/src/api_unstable/dart2js.dart' as fe;
|
|
|
|
import '../compiler_api.dart' as api;
|
|
import 'colors.dart' as colors;
|
|
import 'common/metrics.dart';
|
|
import 'io/source_file.dart';
|
|
import 'util/output_util.dart';
|
|
|
|
abstract class SourceFileByteReader {
|
|
Uint8List getBytes(String filename);
|
|
}
|
|
|
|
abstract class SourceFileProvider implements api.CompilerInput {
|
|
bool get isWindows => Platform.operatingSystem == 'windows';
|
|
Uri cwd = Uri.base;
|
|
int bytesRead = 0;
|
|
int sourceBytesFromDill = 0;
|
|
SourceFileByteReader byteReader;
|
|
final Set<Uri> _registeredUris = {};
|
|
final Map<Uri, Uri> _mappedUris = {};
|
|
final bool disableByteCache;
|
|
final Map<Uri, Uint8List> _byteCache = {};
|
|
|
|
SourceFileProvider(this.byteReader, {this.disableByteCache = true});
|
|
|
|
Future<api.Input<Uint8List>> readBytesFromUri(
|
|
Uri resourceUri,
|
|
api.InputKind inputKind,
|
|
) {
|
|
if (!resourceUri.isAbsolute) {
|
|
resourceUri = cwd.resolveUri(resourceUri);
|
|
}
|
|
if (resourceUri.isScheme('file')) {
|
|
return _readFromFile(resourceUri, inputKind);
|
|
} else {
|
|
throw ArgumentError("Unknown scheme in uri '$resourceUri'");
|
|
}
|
|
}
|
|
|
|
/// Adds [source] to the cache under the [resourceUri] key.
|
|
api.Input<Uint8List> _sourceToFile(
|
|
Uri resourceUri,
|
|
Uint8List source,
|
|
api.InputKind inputKind,
|
|
) {
|
|
switch (inputKind) {
|
|
case api.InputKind.utf8:
|
|
return Utf8BytesSourceFile(resourceUri, source);
|
|
case api.InputKind.binary:
|
|
return Binary(resourceUri, source);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void registerUtf8ContentsForDiagnostics(Uri resourceUri, Uint8List source) {
|
|
if (!resourceUri.isAbsolute) {
|
|
resourceUri = cwd.resolveUri(resourceUri);
|
|
}
|
|
|
|
registerUri(resourceUri);
|
|
|
|
// Source bytes can be empty when the dill has source content erased. In
|
|
// that case we should read the file contents from disk if we need them.
|
|
if (!disableByteCache && source.isNotEmpty) {
|
|
_byteCache[resourceUri] = source;
|
|
}
|
|
sourceBytesFromDill += source.length;
|
|
}
|
|
|
|
/// Registers the URI and returns true if the URI is new.
|
|
bool registerUri(Uri uri) {
|
|
return _registeredUris.add(uri);
|
|
}
|
|
|
|
api.Input<Uint8List> _readFromFileSync(Uri uri, api.InputKind inputKind) {
|
|
final resourceUri = _mappedUris[uri] ?? uri;
|
|
assert(resourceUri.isScheme('file'));
|
|
Uint8List source;
|
|
try {
|
|
source = byteReader.getBytes(resourceUri.toFilePath());
|
|
} on FileSystemException catch (ex) {
|
|
String? message = ex.osError?.message;
|
|
String detail = message != null ? ' ($message)' : '';
|
|
throw "Error reading '${relativizeUri(resourceUri)}' $detail";
|
|
}
|
|
if (registerUri(resourceUri)) {
|
|
bytesRead += source.length;
|
|
}
|
|
if (resourceUri != uri) {
|
|
registerUri(uri);
|
|
}
|
|
return _sourceToFile(Uri.parse(relativizeUri(uri)), source, inputKind);
|
|
}
|
|
|
|
api.Input<Uint8List>? _readFromFileSyncOrNull(
|
|
Uri uri,
|
|
api.InputKind inputKind,
|
|
) {
|
|
try {
|
|
return _readFromFileSync(uri, inputKind);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Read [resourceUri] directly as a UTF-8 file. If reading fails, `null` is
|
|
/// returned.
|
|
api.Input<Uint8List>? readUtf8FromFileSyncForTesting(Uri resourceUri) {
|
|
try {
|
|
return _readFromFileSync(resourceUri, api.InputKind.utf8);
|
|
} catch (e) {
|
|
// Silence the error. The [resourceUri] was not requested by the user and
|
|
// was only needed to give better error messages.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<api.Input<Uint8List>> _readFromFile(
|
|
Uri resourceUri,
|
|
api.InputKind inputKind,
|
|
) {
|
|
api.Input<Uint8List> input;
|
|
try {
|
|
input = _readFromFileSync(resourceUri, inputKind);
|
|
} catch (e) {
|
|
return Future.error(e);
|
|
}
|
|
return Future.value(input);
|
|
}
|
|
|
|
/// Get the bytes for a previously accessed UTF-8 [Uri].
|
|
api.Input<Uint8List>? getUtf8SourceFile(Uri resourceUri) {
|
|
if (!resourceUri.isAbsolute) {
|
|
resourceUri = cwd.resolveUri(resourceUri);
|
|
}
|
|
|
|
if (_byteCache.containsKey(resourceUri)) {
|
|
return _sourceToFile(
|
|
resourceUri,
|
|
_byteCache[resourceUri]!,
|
|
api.InputKind.utf8,
|
|
);
|
|
}
|
|
return resourceUri.isScheme('file')
|
|
? _readFromFileSyncOrNull(resourceUri, api.InputKind.utf8)
|
|
: null;
|
|
}
|
|
|
|
String relativizeUri(Uri uri) => fe.relativizeUri(cwd, uri, isWindows);
|
|
|
|
// Note: this includes also indirect sources that were used to create
|
|
// `.dill` inputs to the compiler. This is OK, since this API is only
|
|
// used to calculate DEPS for gn build systems.
|
|
Iterable<Uri> getSourceUris() => [..._registeredUris, ..._mappedUris.keys];
|
|
}
|
|
|
|
class MemoryCopySourceFileByteReader implements SourceFileByteReader {
|
|
const MemoryCopySourceFileByteReader();
|
|
@override
|
|
Uint8List getBytes(String filename) {
|
|
return readAll(filename);
|
|
}
|
|
}
|
|
|
|
Uint8List readAll(String filename) {
|
|
return File(filename).readAsBytesSync();
|
|
}
|
|
|
|
class CompilerSourceFileProvider extends SourceFileProvider {
|
|
CompilerSourceFileProvider({
|
|
SourceFileByteReader byteReader = const MemoryCopySourceFileByteReader(),
|
|
super.disableByteCache,
|
|
}) : super(byteReader);
|
|
|
|
@override
|
|
Future<api.Input<Uint8List>> readFromUri(
|
|
Uri uri, {
|
|
api.InputKind inputKind = api.InputKind.utf8,
|
|
}) => readBytesFromUri(uri, inputKind);
|
|
}
|
|
|
|
class FormattingDiagnosticHandler implements api.CompilerDiagnostics {
|
|
late final SourceFileProvider provider;
|
|
bool showWarnings = true;
|
|
bool showHints = true;
|
|
bool verbose = false;
|
|
bool isAborting = false;
|
|
bool enableColors = false;
|
|
bool throwOnError = false;
|
|
int throwOnErrorCount = 0;
|
|
api.Diagnostic? lastKind;
|
|
int fatalCount = 0;
|
|
|
|
final int fatalCode =
|
|
api.Diagnostic.crash.ordinal | api.Diagnostic.error.ordinal;
|
|
final int infoCode =
|
|
api.Diagnostic.info.ordinal | api.Diagnostic.verboseInfo.ordinal;
|
|
|
|
FormattingDiagnosticHandler();
|
|
|
|
void registerFileProvider(SourceFileProvider provider) {
|
|
this.provider = provider;
|
|
}
|
|
|
|
void info(
|
|
String message, [
|
|
api.Diagnostic kind = api.Diagnostic.verboseInfo,
|
|
]) {
|
|
if (!verbose && kind == api.Diagnostic.verboseInfo) return;
|
|
if (enableColors) {
|
|
print('${colors.green("Info:")} $message');
|
|
} else {
|
|
print('Info: $message');
|
|
}
|
|
}
|
|
|
|
/// Adds [kind] specific prefix to [message].
|
|
String prefixMessage(String message, api.Diagnostic kind) {
|
|
switch (kind) {
|
|
case api.Diagnostic.error:
|
|
return 'Error: $message';
|
|
case api.Diagnostic.warning:
|
|
return 'Warning: $message';
|
|
case api.Diagnostic.hint:
|
|
return 'Hint: $message';
|
|
case api.Diagnostic.crash:
|
|
return 'Internal Error: $message';
|
|
case api.Diagnostic.context:
|
|
case api.Diagnostic.info:
|
|
case api.Diagnostic.verboseInfo:
|
|
return 'Info: $message';
|
|
}
|
|
}
|
|
|
|
@override
|
|
void report(
|
|
var code,
|
|
Uri? uri,
|
|
int? begin,
|
|
int? end,
|
|
String message,
|
|
api.Diagnostic kind,
|
|
) {
|
|
if (isAborting) return;
|
|
isAborting = (kind == api.Diagnostic.crash);
|
|
|
|
bool fatal = (kind.ordinal & fatalCode) != 0;
|
|
bool isInfo = (kind.ordinal & infoCode) != 0;
|
|
if (isInfo && uri == null && kind != api.Diagnostic.info) {
|
|
info(message, kind);
|
|
return;
|
|
}
|
|
|
|
message = prefixMessage(message, kind);
|
|
|
|
// [lastKind] records the previous non-INFO kind we saw.
|
|
// This is used to suppress info about a warning when warnings are
|
|
// suppressed, and similar for hints.
|
|
if (kind != api.Diagnostic.info) {
|
|
lastKind = kind;
|
|
}
|
|
String Function(String) color;
|
|
if (kind == api.Diagnostic.error) {
|
|
color = colors.red;
|
|
} else if (kind == api.Diagnostic.warning) {
|
|
if (!showWarnings) return;
|
|
color = colors.magenta;
|
|
} else if (kind == api.Diagnostic.hint) {
|
|
if (!showHints) return;
|
|
color = colors.cyan;
|
|
} else if (kind == api.Diagnostic.crash) {
|
|
color = colors.red;
|
|
} else if (kind == api.Diagnostic.info) {
|
|
color = colors.green;
|
|
} else if (kind == api.Diagnostic.context) {
|
|
if (lastKind == api.Diagnostic.warning && !showWarnings) return;
|
|
if (lastKind == api.Diagnostic.hint && !showHints) return;
|
|
color = colors.green;
|
|
} else {
|
|
throw 'Unknown kind: $kind (${kind.ordinal})';
|
|
}
|
|
if (!enableColors) {
|
|
color = (String x) => x;
|
|
}
|
|
if (uri == null) {
|
|
print(color(message));
|
|
} else {
|
|
api.Input<Uint8List>? file = provider.getUtf8SourceFile(uri);
|
|
if (file is SourceFile && begin != null && end != null) {
|
|
print(
|
|
file.getLocationMessage(color(message), begin, end, colorize: color),
|
|
);
|
|
} else {
|
|
String position = begin != null && end != null && end - begin > 0
|
|
? '@$begin+${end - begin}'
|
|
: '';
|
|
print(
|
|
'${provider.relativizeUri(uri)}$position:\n'
|
|
'${color(message)}',
|
|
);
|
|
}
|
|
}
|
|
if (fatal && ++fatalCount >= throwOnErrorCount && throwOnError) {
|
|
isAborting = true;
|
|
throw _CompilationErrorError(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _CompilationErrorError {
|
|
final String message;
|
|
_CompilationErrorError(this.message);
|
|
@override
|
|
String toString() => 'Aborted due to --throw-on-error: $message';
|
|
}
|
|
|
|
typedef OnInfo = void Function(String message);
|
|
typedef OnFailure = Never Function(String message);
|
|
|
|
class RandomAccessFileOutputProvider implements api.CompilerOutput {
|
|
// The file name to use for the main output. Also used as the filename prefix
|
|
// for other URIs generated from this output provider. If `null` there is no
|
|
// primary output but can still write other files.
|
|
final Uri? out;
|
|
final Uri? sourceMapOut;
|
|
final OnInfo onInfo;
|
|
final OnFailure onFailure;
|
|
|
|
int totalCharactersWritten = 0;
|
|
int totalCharactersWrittenPrimary = 0;
|
|
int totalCharactersWrittenJavaScript = 0;
|
|
int totalDataWritten = 0;
|
|
|
|
List<String> allOutputFiles = <String>[];
|
|
|
|
RandomAccessFileOutputProvider(
|
|
this.out,
|
|
this.sourceMapOut, {
|
|
required this.onInfo,
|
|
required this.onFailure,
|
|
});
|
|
|
|
Uri createUri(String name, String extension, api.OutputType type) {
|
|
Uri uri;
|
|
// TODO(johnniwinther): Unify handle of [name] and [extension] to prepare
|
|
// for using a single, possibly relative, [uri] as input.
|
|
switch (type) {
|
|
case api.OutputType.js:
|
|
if (name == '') {
|
|
uri = out!;
|
|
} else {
|
|
uri = out!.resolve('$name.$extension');
|
|
}
|
|
break;
|
|
case api.OutputType.sourceMap:
|
|
if (name == '') {
|
|
uri = sourceMapOut!;
|
|
} else {
|
|
uri = out!.resolve('$name.$extension');
|
|
}
|
|
break;
|
|
case api.OutputType.jsPart:
|
|
uri = out!.resolve('$name.$extension');
|
|
break;
|
|
case api.OutputType.deferredLoadIds:
|
|
assert(name.isNotEmpty);
|
|
return (out ?? Uri.base).resolve(name);
|
|
case api.OutputType.dumpInfo:
|
|
case api.OutputType.dumpUnusedLibraries:
|
|
case api.OutputType.deferredMap:
|
|
case api.OutputType.resourceIdentifiers:
|
|
if (name == '') {
|
|
name = out!.pathSegments.last;
|
|
}
|
|
if (extension == '') {
|
|
uri = out!.resolve(name);
|
|
} else {
|
|
uri = out!.resolve('$name.$extension');
|
|
}
|
|
break;
|
|
case api.OutputType.debug:
|
|
if (name == '') {
|
|
name = out!.pathSegments.last;
|
|
}
|
|
uri = out!.resolve('$name.$extension');
|
|
break;
|
|
}
|
|
return uri;
|
|
}
|
|
|
|
@override
|
|
api.OutputSink createOutputSink(
|
|
String name,
|
|
String extension,
|
|
api.OutputType type,
|
|
) {
|
|
Uri uri = createUri(name, extension, type);
|
|
bool isPrimaryOutput = uri == out;
|
|
|
|
if (!uri.isScheme('file')) {
|
|
onFailure('Unhandled scheme ${uri.scheme} in $uri.');
|
|
}
|
|
|
|
RandomAccessFile output;
|
|
try {
|
|
output = (File(
|
|
uri.toFilePath(),
|
|
)..createSync(recursive: true)).openSync(mode: FileMode.write);
|
|
} on FileSystemException catch (e) {
|
|
onFailure('$e');
|
|
}
|
|
|
|
allOutputFiles.add(fe.relativizeUri(Uri.base, uri, Platform.isWindows));
|
|
|
|
void onClose(int charactersWritten) {
|
|
totalCharactersWritten += charactersWritten;
|
|
if (isPrimaryOutput) {
|
|
totalCharactersWrittenPrimary += charactersWritten;
|
|
}
|
|
if (type == api.OutputType.js || type == api.OutputType.jsPart) {
|
|
totalCharactersWrittenJavaScript += charactersWritten;
|
|
}
|
|
}
|
|
|
|
return BufferedStringSinkWrapper(
|
|
FileStringOutputSink(output, onClose: onClose),
|
|
);
|
|
}
|
|
|
|
@override
|
|
api.BinaryOutputSink createBinarySink(Uri uri) {
|
|
uri = Uri.base.resolveUri(uri);
|
|
|
|
allOutputFiles.add(fe.relativizeUri(Uri.base, uri, Platform.isWindows));
|
|
|
|
if (!uri.isScheme('file')) {
|
|
onFailure('Unhandled scheme ${uri.scheme} in $uri.');
|
|
}
|
|
|
|
RandomAccessFile output;
|
|
try {
|
|
output = (File(
|
|
uri.toFilePath(),
|
|
)..createSync(recursive: true)).openSync(mode: FileMode.write);
|
|
} on FileSystemException catch (e) {
|
|
onFailure('$e');
|
|
}
|
|
|
|
void onClose(int bytesWritten) {
|
|
totalDataWritten += bytesWritten;
|
|
}
|
|
|
|
return FileBinaryOutputSink(output, onClose: onClose);
|
|
}
|
|
}
|
|
|
|
class RandomAccessBinaryOutputSink implements api.BinaryOutputSink {
|
|
final RandomAccessFile output;
|
|
|
|
RandomAccessBinaryOutputSink(Uri uri)
|
|
: output = File.fromUri(uri).openSync(mode: FileMode.write);
|
|
|
|
@override
|
|
void add(List<int> buffer, [int start = 0, int? end]) {
|
|
output.writeFromSync(buffer, start, end);
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
output.closeSync();
|
|
}
|
|
}
|
|
|
|
/// Adapter to integrate dart2js in bazel.
|
|
///
|
|
/// To handle bazel's special layout:
|
|
///
|
|
/// * We specify a .dart_tool/package_config.json configuration file that
|
|
/// expands packages to their corresponding bazel location.
|
|
/// This way there is no need to create a pub
|
|
/// cache prior to invoking dart2js.
|
|
///
|
|
/// * We provide an implicit mapping that can make all urls relative to the
|
|
/// bazel root.
|
|
/// To the compiler, URIs look like:
|
|
/// file:///bazel-root/a/b/c.dart
|
|
///
|
|
/// even though in the file system the file is located at:
|
|
/// file:///path/to/the/actual/bazel/root/a/b/c.dart
|
|
///
|
|
/// This mapping serves two purposes:
|
|
/// - It makes compiler results independent of the machine layout, which
|
|
/// enables us to share results across bazel runs and across machines.
|
|
///
|
|
/// - It hides the distinction between generated and source files. That way
|
|
/// we can use the standard package-resolution mechanism and ignore the
|
|
/// internals of how files are organized within bazel.
|
|
///
|
|
/// When invoking the compiler, bazel will use `package:` and
|
|
/// `file:///bazel-root/` URIs to specify entrypoints.
|
|
///
|
|
/// The mapping is specified using search paths relative to the current
|
|
/// directory. When this provider looks up a file, the bazel-root folder is
|
|
/// replaced by the first directory in the search path containing the file, if
|
|
/// any. For example, given the search path ".,bazel-bin/", and a URL
|
|
/// of the form `file:///bazel-root/a/b.dart`, this provider will check if the
|
|
/// file exists under "./a/b.dart", then check under "bazel-bin/a/b.dart". If
|
|
/// none of the paths matches, it will attempt to load the file from
|
|
/// `/bazel-root/a/b.dart` which will likely fail.
|
|
class BazelInputProvider extends SourceFileProvider {
|
|
final List<Uri> dirs;
|
|
|
|
BazelInputProvider(
|
|
List<String> searchPaths,
|
|
super.byteReader, {
|
|
super.disableByteCache,
|
|
}) : dirs = searchPaths.map(_resolve).toList();
|
|
|
|
static Uri _resolve(String path) => Uri.base.resolve(path);
|
|
|
|
@override
|
|
Future<api.Input<Uint8List>> readFromUri(
|
|
Uri uri, {
|
|
api.InputKind inputKind = api.InputKind.utf8,
|
|
}) async {
|
|
var resolvedUri = uri;
|
|
var path = uri.path;
|
|
if (path.startsWith('/bazel-root')) {
|
|
path = path.substring('/bazel-root/'.length);
|
|
for (var dir in dirs) {
|
|
var file = dir.resolve(path);
|
|
if (await File.fromUri(file).exists()) {
|
|
resolvedUri = file;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
api.Input<Uint8List> result = await readBytesFromUri(
|
|
resolvedUri,
|
|
inputKind,
|
|
);
|
|
if (uri != resolvedUri) {
|
|
if (!resolvedUri.isAbsolute) {
|
|
resolvedUri = cwd.resolveUri(resolvedUri);
|
|
}
|
|
_mappedUris[uri] = resolvedUri;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// Adapter to support one or more synthetic uri schemes.
|
|
///
|
|
/// These custom uris map to one or more real directories on the file system,
|
|
/// providing a merged view - or "overlay" file system.
|
|
///
|
|
/// This also allows for hermetic builds which do not encode machine specific
|
|
/// absolute uris by creating a synthetic "root" of the file system.
|
|
///
|
|
/// TODO(sigmund): Remove the [BazelInputProvider] in favor of this.
|
|
/// TODO(sigmund): Remove this and use the common `MultiRootFileSystem`
|
|
/// implementation.
|
|
class MultiRootInputProvider extends SourceFileProvider {
|
|
final List<Uri> roots;
|
|
final String markerScheme;
|
|
|
|
MultiRootInputProvider(
|
|
this.markerScheme,
|
|
this.roots,
|
|
super.byteReader, {
|
|
super.disableByteCache,
|
|
});
|
|
|
|
@override
|
|
Future<api.Input<Uint8List>> readFromUri(
|
|
Uri uri, {
|
|
api.InputKind inputKind = api.InputKind.utf8,
|
|
}) async {
|
|
var resolvedUri = uri;
|
|
if (resolvedUri.isScheme(markerScheme)) {
|
|
var path = resolvedUri.path;
|
|
if (path.startsWith('/')) path = path.substring(1);
|
|
for (var dir in roots) {
|
|
var fileUri = dir.resolve(path);
|
|
if (await File.fromUri(fileUri).exists()) {
|
|
resolvedUri = fileUri;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
api.Input<Uint8List> result = await readBytesFromUri(
|
|
resolvedUri,
|
|
inputKind,
|
|
);
|
|
_mappedUris[uri] = resolvedUri;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class DataReadMetrics extends MetricsBase {
|
|
@override
|
|
String get namespace => 'input';
|
|
CountMetric inputBytes = CountMetric('inputBytes');
|
|
CountMetric sourceBytes = CountMetric('sourceBytes');
|
|
|
|
void addDataRead(api.CompilerInput input) {
|
|
if (input is SourceFileProvider) {
|
|
inputBytes.add(input.bytesRead);
|
|
sourceBytes.add(input.sourceBytesFromDill);
|
|
if (primary.isEmpty) {
|
|
primary = [inputBytes, sourceBytes];
|
|
}
|
|
}
|
|
}
|
|
}
|