blob: 3028c5f8f895e52e3f2c97dc8093c39626efad39 [file]
// Copyright (c) 2017, 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 specification in-memory representation.
///
/// Many dart tools are configurable to support different target platforms. For
/// a given target, they need to know what libraries are available and where are
/// the sources and target-specific patches.
///
/// Here we define APIs to represent this specification and implement
/// serialization to (and deserialization from) a JSON file.
///
/// Here is an example specification JSON file:
///
/// {
/// "vm": {
/// "libraries": {
/// "core": {
/// "uri": "async/core.dart",
/// "patches": [
/// "path/to/core_patch.dart",
/// "path/to/list_patch.dart"
/// ]
/// }
/// "async": {
/// "uri": "async/async.dart",
/// "patches": "path/to/async_patch.dart"
/// }
/// "convert": {
/// "uri": "convert/convert.dart",
/// }
/// "mirrors": {
/// "uri": "mirrors/mirrors.dart",
/// "support_conditional_import": false
/// }
/// }
/// }
/// }
///
/// The format contains:
/// - a top level entry for each target. Keys are target names (e.g. "vm"
/// above), and values contain the entire specification of a target.
///
/// - each target specification is a map. The supported keys are "libraries"
/// and "include".
///
/// - The "libraries" entry contains details for how each platform library is
/// implemented. The entry is a map, where keys are the name of the platform
/// library and values contain details for where to find the implementation
/// fo that library.
///
/// - The name of the library is a single token (e.g. "core") that matches the
/// Uri path used after `dart:` (e.g. "dart:core").
///
/// - The "uri" entry on the library information is mandatory. The value is a
/// string URI reference. The "patches" entry is optional and may have as a
/// value a string URI reference or a list of URI references.
///
/// All URI references can either be a file URI or a relative URI path,
/// which will be resolved relative to the location of the library
/// specification file.
///
/// - The "support_conditional_import" entry on the library information is
/// optional. The value is a boolean indicating whether the library is
/// supported in the underlying target. However, since the libraries are
/// assumed to be supported by default, we only expect users to use
/// `false`.
///
/// The purpose of this value is to configure conditional imports and
/// environment constants. By default every platform library that is
/// available in the "libraries" section implicitly defines an environment
/// variable `dart.library.name` as `"true"`, to indicate that the library
/// is supported. Some backends allow imports to an unsupported platform
/// library (turning a static error into a runtime error when the library
/// is eventually accessed). These backends can use
/// `support_conditional_import: false` to report that such library is
/// still not supported in conditional imports and const `fromEnvironment`
/// expressions.
///
/// Internal libraries are never supported through conditional imports and
/// const `fromEnvironment` expressions.
///
/// - The "support_direct_import" entry on the library information is
/// optional. The value is an enumerated type indicating whether the library
/// is allowed to be imported by the underlying target. This property can
/// take one of the following values:
///
/// - "always": the library is supported by the underlying target and
/// can always be imported. This is the default.
/// - "never": the library is not supported by the underlying target and
/// imports of the library will be treated as compile time errors.
/// - "with_flag": the library is only supported when the
/// `--include-unsupported-platform-library-stubs` flag is set. If the
/// flag is not set, imports of the library will be treated as
/// compile time errors. Otherwise, the import will be allowed.
///
/// Some backends allow imports to an unsupported platform library (turning
/// a static error into a runtime error when the library is eventually
/// accessed). These backends can use `support_direct_import: "with_flag"`
/// to control access to these libraries, allowing for developer tooling to
/// handle code which imports unsupported libraries for a given platform.
///
/// - The "include" entry is a list of maps, each containing either a "path"
/// and a "target" entry, or only a "target" entry.
///
/// If both "path" and "target" entries are present, the libraries
/// specification file located at "path", relative to the location current
/// libraries specification file is loaded, and the libraries defined
/// in the target with the target name of the "target" entry are included in
/// this target.
///
/// If only the "target" is present, the libraries in the target in the
/// current libraries specification file with the target name of the
/// "target" entry are included in this target.
///
/// The "include" mechanism support transitive inclusion but doesn't allow
/// cyclic dependencies.
///
/// If the same library is defined in multiple included target
/// specifications, the last included takes precedence. This means that
/// if a target specification include a library and also defines it itself,
/// the latter is used.
///
/// Currently it is not supported to include a subset of the libraries from
/// an included target specifications.
///
///
/// Note: we currently have several different files that need to be updated
/// when changing libraries, sources, and patch files:
/// * .platform files (for dart2js)
/// * .gypi files (for vm)
/// * sdk_library_metadata/lib/libraries.dart (for analyzer, ddc)
///
/// we are in the process of unifying them all under this format (see
/// https://github.com/dart-lang/sdk/issues/28836), but for now we need to pay
/// close attention to change them consistently.
import 'dart:convert' show jsonDecode, jsonEncode;
import 'relativize.dart' show relativizeUri, isWindows;
/// Contents from a single library specification file.
///
/// Contains information about all libraries on all target platforms defined in
/// that file.
class LibrariesSpecification {
final Uri specUri;
final Map<String, TargetLibrariesSpecification> _targets;
const LibrariesSpecification(
this.specUri, [
this._targets = const <String, TargetLibrariesSpecification>{},
]);
/// The library specification for a given [target], or throws if none is
/// available.
TargetLibrariesSpecification specificationFor(String target) {
TargetLibrariesSpecification? targetSpec = _targets[target];
if (targetSpec == null) {
throw new LibrariesSpecificationException(
messageMissingTarget(target, specUri),
);
}
return targetSpec;
}
static Future<LibrariesSpecification> load(
Uri uri,
Future<String> Function(Uri uri) read,
) {
Map<Uri, LibrariesSpecification?> cache = {};
Future<LibrariesSpecification> loadSpecification(Uri uri) async {
if (cache.containsKey(uri)) {
LibrariesSpecification? specification = cache[uri];
if (specification == null) {
throw new LibrariesSpecificationException(messageCyclicSpec(uri));
}
return specification;
}
cache[uri] = null;
String json;
try {
json = await read(uri);
} catch (e) {
throw new LibrariesSpecificationException(
messageIncludePathCouldNotBeRead(uri, e),
);
}
return cache[uri] = await LibrariesSpecification.parse(
uri,
json,
loadSpecification,
);
}
return loadSpecification(uri);
}
/// Parse the given [json] as a library specification, resolving any relative
/// paths from [specUri].
///
/// May throw an exception if [json] is not properly formatted or contains
/// invalid values.
static Future<LibrariesSpecification> parse(
Uri specUri,
String? json,
Future<LibrariesSpecification> Function(Uri uri) loadSpecification,
) async {
if (json == null) return new LibrariesSpecification(specUri);
Map<String, dynamic> jsonData;
try {
dynamic data = jsonDecode(json);
if (data is! Map<String, dynamic>) {
return _reportError(messageTopLevelIsNotAMap(specUri));
}
jsonData = data;
} on FormatException catch (e) {
throw new LibrariesSpecificationException(e);
}
Map<String, TargetLibrariesSpecification> targets =
<String, TargetLibrariesSpecification>{};
Set<String> currentTargets = {};
Future<TargetLibrariesSpecification> resolveTargetData(
String targetName,
) async {
TargetLibrariesSpecification? spec = targets[targetName];
if (spec != null) {
return spec;
}
if (currentTargets.contains(targetName)) {
_reportError(messageCyclicInternalInclude(targetName, specUri));
}
currentTargets.add(targetName);
Map<String, LibraryInfo> libraries = <String, LibraryInfo>{};
Object? targetData = jsonData[targetName];
if (targetData is! Map) {
_reportError(messageTargetIsNotAMap(targetName, specUri));
}
Object? include = targetData["include"];
if (include != null) {
if (include is! List) {
_reportError(messageIncludeIsNotAList(targetName, specUri));
}
for (Object? map in include) {
if (map is! Map<String, dynamic>) {
_reportError(messageIncludeEntryIsNotAMap(targetName, specUri));
}
if (!map.containsKey("target")) {
_reportError(messageIncludeTargetMissing(targetName, specUri));
}
Object? target = map["target"];
if (target is! String) {
_reportError(messageIncludeTargetIsNotAString(targetName, specUri));
}
if (!map.containsKey("path")) {
if (!jsonData.containsKey(target)) {
_reportError(messageMissingTarget(target, specUri));
}
TargetLibrariesSpecification targetLibrariesSpecification =
await resolveTargetData(target);
libraries.addAll(targetLibrariesSpecification._libraries);
} else {
Object? path = map["path"];
if (path is! String) {
_reportError(messageIncludePathIsNotAString(targetName, specUri));
}
Uri uri = Uri.parse(path);
if (uri.hasScheme && !uri.isScheme('file')) {
return _reportError(messageUnsupportedUriScheme(path, specUri));
}
LibrariesSpecification specification = await loadSpecification(
specUri.resolveUri(uri),
);
TargetLibrariesSpecification targetSpecification = specification
.specificationFor(target);
for (LibraryInfo libraryInfo in targetSpecification.allLibraries) {
libraries[libraryInfo.name] = libraryInfo;
}
}
}
}
if (!targetData.containsKey("libraries")) {
_reportError(messageTargetLibrariesMissing(targetName, specUri));
}
Object? librariesData = targetData["libraries"];
if (librariesData is! Map<String, dynamic>) {
_reportError(messageLibrariesEntryIsNotAMap(targetName, specUri));
}
librariesData.forEach((String libraryName, Object? data) {
if (data is! Map<String, dynamic>) {
_reportError(
messageLibraryDataIsNotAMap(libraryName, targetName, specUri),
);
}
Uri checkAndResolve(Object? uriString) {
if (uriString is! String) {
return _reportError(
messageLibraryUriIsNotAString(
uriString,
libraryName,
targetName,
specUri,
),
);
}
Uri uri = Uri.parse(uriString);
if (uri.hasScheme && !uri.isScheme('file')) {
return _reportError(
messageUnsupportedUriScheme(uriString, specUri),
);
}
return specUri.resolveUri(uri);
}
if (!data.containsKey('uri')) {
_reportError(
messageLibraryUriMissing(libraryName, targetName, specUri),
);
}
Uri uri = checkAndResolve(data['uri']);
List<Uri> patches;
if (data['patches'] is List) {
patches = data['patches']
.map<Uri>((s) => specUri.resolve(s))
.toList();
} else if (data['patches'] is String) {
patches = [checkAndResolve(data['patches'])];
} else if (data['patches'] == null) {
patches = const [];
} else {
_reportError(messagePatchesMustBeListOrString(libraryName));
}
final Object supportConditionalImport =
data['support_conditional_import'] ?? true;
if (supportConditionalImport is! bool) {
_reportError(
messagePropertyIsNotABool(
'support_conditional_import',
supportConditionalImport,
),
);
}
final Object? supportDirectImportRaw = data['support_direct_import'];
final Importability? importability = supportDirectImportRaw == null
? Importability.always
: Importability.fromJson(supportDirectImportRaw);
if (importability == null) {
_reportError(
messageSupportDirectImportIsNotValidValue(supportDirectImportRaw!),
);
}
libraries[libraryName] = new LibraryInfo(
libraryName,
uri,
patches,
// Internal libraries are never supported through conditional
// imports and const `fromEnvironment` expressions.
supportConditionalImport:
supportConditionalImport && !libraryName.startsWith('_'),
importability: importability,
);
});
currentTargets.remove(targetName);
return targets[targetName] = new TargetLibrariesSpecification(
targetName,
libraries,
);
}
for (String targetName in jsonData.keys) {
if (targetName.startsWith("comment:")) {
continue;
}
await resolveTargetData(targetName);
}
return new LibrariesSpecification(specUri, targets);
}
static Never _reportError(String error) =>
throw new LibrariesSpecificationException(error);
/// Serialize this specification to json.
///
/// If possible serializes paths relative to [outputUri].
String toJsonString(Uri outputUri) => jsonEncode(toJsonMap(outputUri));
Map toJsonMap(Uri outputUri) {
Map result = {};
Uri dir = outputUri.resolve('.');
String pathFor(Uri uri) => relativizeUri(dir, uri, isWindows);
_targets.forEach((targetName, target) {
Map libraries = {};
target._libraries.forEach((name, lib) {
libraries[name] = {
'uri': pathFor(lib.uri),
'patches': lib.patches.map(pathFor).toList(),
};
if (!lib.supportConditionalImport) {
libraries[name]['support_conditional_import'] = false;
}
if (lib.importability != Importability.always) {
libraries[name]['support_direct_import'] = lib.importability.value;
}
});
result[targetName] = {'libraries': libraries};
});
return result;
}
}
/// Determines whether or not a `dart:*` library is importable for a given
/// platform.
enum Importability {
/// This `dart:*` library is always importable on the target platform.
always(value: 'always'),
/// This `dart:*` library is only importable on the target platform when
/// `--include-unsupported-platform-library-stubs` is provided to the CFE.
withFlag(value: 'with_flag'),
/// This `dart:*` library is never importable on the target platform.
never(value: 'never');
const Importability({required this.value});
final String value;
static Importability? fromJson(Object? value) {
if (value is! String) return null;
if (value == always.value) return always;
if (value == withFlag.value) return withFlag;
if (value == never.value) return never;
return null;
}
}
/// Specifies information about all libraries supported by a given target.
class TargetLibrariesSpecification {
/// Name of the target platform.
final String targetName;
final Map<String, LibraryInfo> _libraries;
const TargetLibrariesSpecification(
this.targetName, [
this._libraries = const <String, LibraryInfo>{},
]);
/// Details about a library whose import is `dart:$name`.
LibraryInfo? libraryInfoFor(String name) => _libraries[name];
Iterable<LibraryInfo> get allLibraries => _libraries.values;
}
/// Information about a `dart:` library in a specific target platform.
class LibraryInfo {
/// The name of the library, which is the path developers use to import this
/// library (as `dart:$name`).
final String name;
/// The file defining the main implementation of the library.
final Uri uri;
/// Patch files used for this library in the target platform, if any.
final List<Uri> patches;
/// Whether the library is supported and thus `dart.library.name` is "true"
/// for conditional imports and fromEnvironment constants.
final bool supportConditionalImport;
/// Whether the library is importable for a given target platform.
///
/// If not explicitly provided, this field defaults to [Importability.always].
final Importability importability;
const LibraryInfo(
this.name,
this.uri,
this.patches, {
this.supportConditionalImport = true,
this.importability = Importability.always,
});
/// The import uri for the defined library.
Uri get importUri => Uri.parse('dart:${name}');
}
class LibrariesSpecificationException {
Object error;
LibrariesSpecificationException(this.error);
@override
String toString() => '$error';
}
String messageMissingTarget(String targetName, Uri specUri) =>
'No library specification for target "$targetName" in ${specUri}.';
String messageCyclicSpec(Uri specUri) => 'Cyclic dependency in ${specUri}.';
String messageCyclicInternalInclude(String targetName, Uri specUri) =>
'Cyclic dependency of target "$targetName" in ${specUri}.';
String messageTopLevelIsNotAMap(Uri specUri) =>
'Top-level specification is not a map in ${specUri}.';
String messageTargetIsNotAMap(String targetName, Uri specUri) =>
'Target specification for "$targetName" is not a map in $specUri.';
String messageIncludeIsNotAList(String targetName, Uri specUri) =>
'"include" specification for "$targetName" is not a list in $specUri.';
String messageIncludeEntryIsNotAMap(String targetName, Uri specUri) =>
'"include" entry in "$targetName" is not a map in $specUri.';
String messageIncludePathIsNotAString(String targetName, Uri specUri) =>
'"include" path in "$targetName" is not a string in $specUri.';
String messageIncludePathCouldNotBeRead(Uri includeUri, Object error) =>
'"include" path \'$includeUri\' could not be read: $error';
String messageIncludeTargetMissing(String targetName, Uri specUri) =>
'"include" target in "$targetName" is missing in $specUri.';
String messageIncludeTargetIsNotAString(String targetName, Uri specUri) =>
'"include" target in "$targetName" is not a string in $specUri.';
String messageTargetLibrariesMissing(String targetName, Uri specUri) =>
'Target specification '
'for "$targetName" doesn\'t have a libraries entry in $specUri.';
String messageLibrariesEntryIsNotAMap(String targetName, Uri specUri) =>
'"libraries" entry for "$targetName" is not a map in $specUri.';
String messageLibraryDataIsNotAMap(
String libraryName,
String targetName,
Uri specUri,
) =>
'Library data for \'$libraryName\' in target "$targetName" is not a map '
'in $specUri.';
String messageLibraryUriMissing(
String libraryName,
String targetName,
Uri specUri,
) =>
'"uri" is missing '
'from library \'$libraryName\' in target "$targetName" in $specUri.';
String messageLibraryUriIsNotAString(
Object? uriValue,
String libraryName,
String targetName,
Uri specUri,
) =>
'Uri value `$uriValue` is not a string '
'(from library \'$libraryName\' in target "$targetName" in $specUri).';
String messageUnsupportedUriScheme(String uriValue, Uri specUri) =>
"Uri scheme in '$uriValue' is not supported in $specUri.";
String messagePatchesMustBeListOrString(String libraryName) =>
'"patches" entry for "$libraryName" is not a list or a string.';
String messagePropertyIsNotABool(String key, Object value) =>
'"$key" entry: expected a `bool` but '
'got a `${value.runtimeType}` ("$value").';
String messageOnlyOnePropertyCanBeDefined(List<String> properties) {
return 'Only one of the following properties can be defined: '
'${properties.map((e) => '`$e`').join(',')}.';
}
String messageSupportDirectImportIsNotValidValue(Object value) {
final String values = Importability.values
.map((e) => '`${e.value}`')
.join(',');
return '"support_direct_import" entry: expected one of $values but got a '
'`${value.runtimeType}` ("$value").';
}