blob: e368e616f4def182116e27ce9e73eb219e8d49d7 [file] [log] [blame] [edit]
// 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];
}
}
}
}