| // 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").'; | |
| } |