| // 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", |
| /// "supported": 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. Today, only one key is supported on |
| /// this map: "libraries". |
| /// |
| /// - 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 "supported" 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 `supported: 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. |
| /// |
| /// |
| /// 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.scheme != '' && uri.scheme != '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.scheme != '' && uri.scheme != '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)); |
| } |
| |
| dynamic supported = data['supported'] ?? true; |
| if (supported is! bool) { |
| _reportError(messageSupportedIsNotABool(supported)); |
| } |
| libraries[libraryName] = new LibraryInfo(libraryName, uri, patches, |
| // Internal libraries are never supported through conditional |
| // imports and const `fromEnvironment` expressions. |
| isSupported: supported && !libraryName.startsWith('_')); |
| }); |
| 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.isSupported) { |
| libraries[name]['supported'] = false; |
| } |
| }); |
| result[targetName] = {'libraries': libraries}; |
| }); |
| return result; |
| } |
| } |
| |
| /// 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 isSupported; |
| |
| const LibraryInfo(this.name, this.uri, this.patches, |
| {this.isSupported: true}); |
| |
| /// 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 messageSupportedIsNotABool(Object supportedValue) => |
| '"supported" entry: expected a `bool` but ' |
| 'got a `${supportedValue.runtimeType}` ("$supportedValue").'; |