| // Copyright (c) 2012, 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. |
| |
| import 'dart:io'; |
| |
| import 'package:collection/collection.dart' hide mapMap; |
| import 'package:path/path.dart' as path; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:source_span/source_span.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'exceptions.dart'; |
| import 'feature.dart'; |
| import 'io.dart'; |
| import 'language_version.dart'; |
| import 'log.dart'; |
| import 'package_name.dart'; |
| import 'pubspec_parse.dart'; |
| import 'sdk.dart'; |
| import 'source_registry.dart'; |
| import 'utils.dart'; |
| |
| export 'pubspec_parse.dart' hide PubspecBase; |
| |
| /// The default SDK upper bound constraint for packages that don't declare one. |
| /// |
| /// This provides a sane default for packages that don't have an upper bound. |
| final VersionRange _defaultUpperBoundSdkConstraint = |
| VersionConstraint.parse('<2.0.0') as VersionRange; |
| |
| /// Whether or not to allow the pre-release SDK for packages that have an |
| /// upper bound Dart SDK constraint of <2.0.0. |
| /// |
| /// If enabled then a Dart SDK upper bound of <2.0.0 is always converted to |
| /// <2.0.0-dev.infinity. |
| /// |
| /// This has a default value of `true` but can be overridden with the |
| /// PUB_ALLOW_PRERELEASE_SDK system environment variable. |
| bool get _allowPreReleaseSdk => _allowPreReleaseSdkValue != 'false'; |
| |
| /// The value of the PUB_ALLOW_PRERELEASE_SDK environment variable, defaulted |
| /// to `true`. |
| final String _allowPreReleaseSdkValue = () { |
| var value = |
| Platform.environment['PUB_ALLOW_PRERELEASE_SDK']?.toLowerCase() ?? 'true'; |
| if (!['true', 'quiet', 'false'].contains(value)) { |
| warning(yellow(''' |
| The environment variable PUB_ALLOW_PRERELEASE_SDK is set as `$value`. |
| The expected value is either `true`, `quiet` (true but no logging), or `false`. |
| Using a default value of `true`. |
| ''')); |
| value = 'true'; |
| } |
| return value; |
| }(); |
| |
| /// Whether or not to warn about pre-release SDK overrides. |
| bool get warnAboutPreReleaseSdkOverrides => _allowPreReleaseSdkValue != 'quiet'; |
| |
| /// The parsed contents of a pubspec file. |
| /// |
| /// The fields of a pubspec are, for the most part, validated when they're first |
| /// accessed. This allows a partially-invalid pubspec to be used if only the |
| /// valid portions are relevant. To get a list of all errors in the pubspec, use |
| /// [allErrors]. |
| class Pubspec extends PubspecBase { |
| // If a new lazily-initialized field is added to this class and the |
| // initialization can throw a [PubspecException], that error should also be |
| // exposed through [allErrors]. |
| |
| /// The registry of sources to use when parsing [dependencies] and |
| /// [devDependencies]. |
| /// |
| /// This will be null if this was created using [new Pubspec] or [new |
| /// Pubspec.empty]. |
| final SourceRegistry? _sources; |
| |
| /// The location from which the pubspec was loaded. |
| /// |
| /// This can be null if the pubspec was created in-memory or if its location |
| /// is unknown. |
| Uri? get _location => fields.span.sourceUrl; |
| |
| /// The additional packages this package depends on. |
| Map<String, PackageRange> get dependencies => _dependencies ??= |
| _parseDependencies('dependencies', fields.nodes['dependencies']); |
| |
| Map<String, PackageRange>? _dependencies; |
| |
| /// The packages this package depends on when it is the root package. |
| Map<String, PackageRange> get devDependencies => _devDependencies ??= |
| _parseDependencies('dev_dependencies', fields.nodes['dev_dependencies']); |
| |
| Map<String, PackageRange>? _devDependencies; |
| |
| /// The dependency constraints that this package overrides when it is the |
| /// root package. |
| /// |
| /// Dependencies here will replace any dependency on a package with the same |
| /// name anywhere in the dependency graph. |
| Map<String, PackageRange> get dependencyOverrides => |
| _dependencyOverrides ??= _parseDependencies( |
| 'dependency_overrides', fields.nodes['dependency_overrides']); |
| |
| Map<String, PackageRange>? _dependencyOverrides; |
| |
| late final Map<String, Feature> features = _computeFeatures(); |
| |
| Map<String, Feature> _computeFeatures() { |
| final features = fields['features']; |
| if (features == null) { |
| return const {}; |
| } |
| |
| if (features is! YamlMap) { |
| _error('"features" field must be a map.', fields.nodes['features']!.span); |
| } |
| |
| return mapMap(features.nodes, |
| key: (dynamic nameNode, dynamic _) => _validateFeatureName(nameNode), |
| value: (dynamic nameNode, dynamic specNode) { |
| if (specNode.value == null) { |
| return Feature(nameNode.value, const []); |
| } |
| |
| if (specNode is! YamlMap) { |
| _error('A feature specification must be a map.', specNode.span); |
| } |
| |
| var onByDefault = specNode['default'] ?? true; |
| if (onByDefault is! bool) { |
| _error('Default must be true or false.', |
| specNode.nodes['default']!.span); |
| } |
| |
| var requires = _parseStringList(specNode.nodes['requires'], |
| validate: (name, span) { |
| if (!features.containsKey(name)) _error('Undefined feature.', span); |
| }); |
| |
| var dependencies = _parseDependencies( |
| 'dependencies', specNode.nodes['dependencies']); |
| |
| var sdkConstraints = _parseEnvironment(specNode); |
| |
| return Feature(nameNode.value, dependencies.values, |
| requires: requires, |
| sdkConstraints: sdkConstraints, |
| onByDefault: onByDefault); |
| }); |
| } |
| |
| /// A map from SDK identifiers to constraints on those SDK versions. |
| Map<String, VersionConstraint> get sdkConstraints { |
| _ensureEnvironment(); |
| return _sdkConstraints!; |
| } |
| |
| Map<String, VersionConstraint>? _sdkConstraints; |
| |
| /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this |
| /// pubspec. |
| final bool _includeDefaultSdkConstraint; |
| |
| /// Whether or not the SDK version was overridden from <2.0.0 to |
| /// <2.0.0-dev.infinity. |
| bool get dartSdkWasOverridden => _dartSdkWasOverridden; |
| bool _dartSdkWasOverridden = false; |
| |
| /// The original Dart SDK constraint as written in the pubspec. |
| /// |
| /// If [dartSdkWasOverridden] is `false`, this will be identical to |
| /// `sdkConstraints["dart"]`. |
| VersionConstraint get originalDartSdkConstraint { |
| _ensureEnvironment(); |
| return _originalDartSdkConstraint ?? sdkConstraints['dart']!; |
| } |
| |
| VersionConstraint? _originalDartSdkConstraint; |
| |
| /// Ensures that the top-level "environment" field has been parsed and |
| /// [_sdkConstraints] is set accordingly. |
| void _ensureEnvironment() { |
| if (_sdkConstraints != null) return; |
| |
| var sdkConstraints = _parseEnvironment(fields); |
| var parsedDartSdkConstraint = sdkConstraints['dart']; |
| |
| if (parsedDartSdkConstraint is VersionRange && |
| _shouldEnableCurrentSdk(parsedDartSdkConstraint)) { |
| _originalDartSdkConstraint = parsedDartSdkConstraint; |
| _dartSdkWasOverridden = true; |
| sdkConstraints['dart'] = VersionRange( |
| min: parsedDartSdkConstraint.min, |
| includeMin: parsedDartSdkConstraint.includeMin, |
| max: sdk.version, |
| includeMax: true); |
| } |
| |
| _sdkConstraints = UnmodifiableMapView(sdkConstraints); |
| } |
| |
| /// Whether or not we should override [sdkConstraint] to be <= the user's |
| /// current SDK version. |
| /// |
| /// This is true if the following conditions are met: |
| /// |
| /// - [_allowPreReleaseSdk] is `true` |
| /// - The user's current SDK is a pre-release version. |
| /// - The original [sdkConstraint] max version is exclusive (`includeMax` |
| /// is `false`). |
| /// - The original [sdkConstraint] is not a pre-release version. |
| /// - The original [sdkConstraint] matches the exact same major, minor, and |
| /// patch versions as the user's current SDK. |
| bool _shouldEnableCurrentSdk(VersionRange sdkConstraint) { |
| if (!_allowPreReleaseSdk) return false; |
| if (!sdk.version.isPreRelease) return false; |
| if (sdkConstraint.includeMax) return false; |
| var minSdkConstraint = sdkConstraint.min; |
| if (minSdkConstraint != null && |
| minSdkConstraint.isPreRelease && |
| equalsIgnoringPreRelease(sdkConstraint.min!, sdk.version)) { |
| return false; |
| } |
| var maxSdkConstraint = sdkConstraint.max; |
| if (maxSdkConstraint == null) return false; |
| if (maxSdkConstraint.max.isPreRelease && |
| !maxSdkConstraint.isFirstPreRelease) { |
| return false; |
| } |
| return equalsIgnoringPreRelease(maxSdkConstraint, sdk.version); |
| } |
| |
| /// Parses the "environment" field in [parent] and returns a map from SDK |
| /// identifiers to constraints on those SDKs. |
| Map<String, VersionConstraint> _parseEnvironment(YamlMap parent) { |
| var yaml = parent['environment']; |
| if (yaml == null) { |
| return { |
| 'dart': _includeDefaultSdkConstraint |
| ? _defaultUpperBoundSdkConstraint |
| : VersionConstraint.any |
| }; |
| } |
| |
| if (yaml is! YamlMap) { |
| _error('"environment" field must be a map.', |
| parent.nodes['environment']!.span); |
| } |
| |
| var constraints = { |
| 'dart': _parseVersionConstraint(yaml.nodes['sdk'], |
| defaultUpperBoundConstraint: _includeDefaultSdkConstraint |
| ? _defaultUpperBoundSdkConstraint |
| : null) |
| }; |
| yaml.nodes.forEach((name, constraint) { |
| if (name.value is! String) { |
| _error('SDK names must be strings.', name.span); |
| } else if (name.value == 'dart') { |
| _error('Use "sdk" to for Dart SDK constraints.', name.span); |
| } |
| if (name.value == 'sdk') return; |
| |
| constraints[name.value as String] = _parseVersionConstraint(constraint, |
| // Flutter constraints get special treatment, as Flutter won't be |
| // using semantic versioning to mark breaking releases. |
| ignoreUpperBound: name.value == 'flutter'); |
| }); |
| |
| return constraints; |
| } |
| |
| /// The language version implied by the sdk constraint. |
| LanguageVersion get languageVersion => |
| LanguageVersion.fromSdkConstraint(originalDartSdkConstraint); |
| |
| /// Loads the pubspec for a package located in [packageDir]. |
| /// |
| /// If [expectedName] is passed and the pubspec doesn't have a matching name |
| /// field, this will throw a [PubspecException]. |
| factory Pubspec.load(String packageDir, SourceRegistry sources, |
| {String? expectedName}) { |
| var pubspecPath = path.join(packageDir, 'pubspec.yaml'); |
| var pubspecUri = path.toUri(pubspecPath); |
| if (!fileExists(pubspecPath)) { |
| throw FileException( |
| // Make the package dir absolute because for the entrypoint it'll just |
| // be ".", which may be confusing. |
| 'Could not find a file named "pubspec.yaml" in ' |
| '"${canonicalize(packageDir)}".', |
| pubspecPath); |
| } |
| |
| return Pubspec.parse(readTextFile(pubspecPath), sources, |
| expectedName: expectedName, location: pubspecUri); |
| } |
| |
| Pubspec(String name, |
| {Version? version, |
| Iterable<PackageRange>? dependencies, |
| Iterable<PackageRange>? devDependencies, |
| Iterable<PackageRange>? dependencyOverrides, |
| Map? fields, |
| SourceRegistry? sources, |
| Map<String, VersionConstraint>? sdkConstraints}) |
| : _dependencies = dependencies == null |
| ? null |
| : Map.fromIterable(dependencies, key: (range) => range.name), |
| _devDependencies = devDependencies == null |
| ? null |
| : Map.fromIterable(devDependencies, key: (range) => range.name), |
| _dependencyOverrides = dependencyOverrides == null |
| ? null |
| : Map.fromIterable(dependencyOverrides, key: (range) => range.name), |
| _sdkConstraints = sdkConstraints ?? |
| UnmodifiableMapView({'dart': VersionConstraint.any}), |
| _includeDefaultSdkConstraint = false, |
| _sources = sources, |
| super( |
| fields == null ? YamlMap() : YamlMap.wrap(fields), |
| name: name, |
| version: version, |
| ); |
| Pubspec.empty() |
| : _sources = null, |
| _dependencies = {}, |
| _devDependencies = {}, |
| _sdkConstraints = {'dart': VersionConstraint.any}, |
| _includeDefaultSdkConstraint = false, |
| super( |
| YamlMap(), |
| version: Version.none, |
| ); |
| |
| /// Returns a Pubspec object for an already-parsed map representing its |
| /// contents. |
| /// |
| /// If [expectedName] is passed and the pubspec doesn't have a matching name |
| /// field, this will throw a [PubspecError]. |
| /// |
| /// [location] is the location from which this pubspec was loaded. |
| Pubspec.fromMap(Map fields, this._sources, |
| {String? expectedName, Uri? location}) |
| : _includeDefaultSdkConstraint = true, |
| super(fields is YamlMap |
| ? fields |
| : YamlMap.wrap(fields, sourceUrl: location)) { |
| // If [expectedName] is passed, ensure that the actual 'name' field exists |
| // and matches the expectation. |
| if (expectedName == null) return; |
| if (name == expectedName) return; |
| |
| throw PubspecException( |
| '"name" field doesn\'t match expected name ' |
| '"$expectedName".', |
| this.fields.nodes['name']!.span); |
| } |
| |
| /// Parses the pubspec stored at [filePath] whose text is [contents]. |
| /// |
| /// If the pubspec doesn't define a version for itself, it defaults to |
| /// [Version.none]. |
| factory Pubspec.parse(String contents, SourceRegistry sources, |
| {String? expectedName, Uri? location}) { |
| YamlNode pubspecNode; |
| try { |
| pubspecNode = loadYamlNode(contents, sourceUrl: location); |
| } on YamlException catch (error) { |
| throw PubspecException(error.message, error.span); |
| } |
| |
| Map pubspecMap; |
| if (pubspecNode is YamlScalar && pubspecNode.value == null) { |
| pubspecMap = YamlMap(sourceUrl: location); |
| } else if (pubspecNode is YamlMap) { |
| pubspecMap = pubspecNode; |
| } else { |
| throw PubspecException( |
| 'The pubspec must be a YAML mapping.', pubspecNode.span); |
| } |
| |
| return Pubspec.fromMap(pubspecMap, sources, |
| expectedName: expectedName, location: location); |
| } |
| |
| /// Returns a list of most errors in this pubspec. |
| /// |
| /// This will return at most one error for each field. |
| List<PubspecException> get allErrors { |
| var errors = <PubspecException>[]; |
| void _collectError(void Function() fn) { |
| try { |
| fn(); |
| } on PubspecException catch (e) { |
| errors.add(e); |
| } |
| } |
| |
| _collectError(() => name); |
| _collectError(() => version); |
| _collectError(() => dependencies); |
| _collectError(() => devDependencies); |
| _collectError(() => publishTo); |
| _collectError(() => features); |
| _collectError(() => executables); |
| _collectError(() => falseSecrets); |
| _collectError(_ensureEnvironment); |
| return errors; |
| } |
| |
| /// Parses the dependency field named [field], and returns the corresponding |
| /// map of dependency names to dependencies. |
| Map<String, PackageRange> _parseDependencies(String field, YamlNode? node) { |
| var dependencies = <String, PackageRange>{}; |
| |
| // Allow an empty dependencies key. |
| if (node == null || node.value == null) return dependencies; |
| |
| if (node is! YamlMap) { |
| _error('"$field" field must be a map.', node.span); |
| } |
| |
| var nonStringNode = node.nodes.keys |
| .firstWhere((e) => e.value is! String, orElse: () => null); |
| if (nonStringNode != null) { |
| _error('A dependency name must be a string.', nonStringNode.span); |
| } |
| |
| node.nodes.forEach((nameNode, specNode) { |
| var name = nameNode.value; |
| var spec = specNode.value; |
| if (fields['name'] != null && name == this.name) { |
| _error('A package may not list itself as a dependency.', nameNode.span); |
| } |
| |
| YamlNode? descriptionNode; |
| String? sourceName; |
| |
| VersionConstraint versionConstraint = VersionRange(); |
| var features = const <String, FeatureDependency>{}; |
| if (spec == null) { |
| sourceName = _sources!.defaultSource.name; |
| } else if (spec is String) { |
| sourceName = _sources!.defaultSource.name; |
| versionConstraint = _parseVersionConstraint(specNode); |
| } else if (spec is Map) { |
| // Don't write to the immutable YAML map. |
| spec = Map.from(spec); |
| var specMap = specNode as YamlMap; |
| |
| if (spec.containsKey('version')) { |
| spec.remove('version'); |
| versionConstraint = _parseVersionConstraint(specMap.nodes['version']); |
| } |
| |
| if (spec.containsKey('features')) { |
| spec.remove('features'); |
| features = _parseDependencyFeatures(specMap.nodes['features']); |
| } |
| |
| var sourceNames = spec.keys.toList(); |
| if (sourceNames.length > 1) { |
| _error('A dependency may only have one source.', specNode.span); |
| } else if (sourceNames.isEmpty) { |
| // Default to a hosted dependency if no source is specified. |
| sourceName = 'hosted'; |
| } |
| |
| sourceName ??= sourceNames.single; |
| if (sourceName is! String) { |
| _error('A source name must be a string.', |
| specMap.nodes.keys.single.span); |
| } |
| |
| descriptionNode ??= specMap.nodes[sourceName]; |
| } else { |
| _error('A dependency specification must be a string or a mapping.', |
| specNode.span); |
| } |
| |
| // Let the source validate the description. |
| var ref = _wrapFormatException('description', descriptionNode?.span, () { |
| String? pubspecPath; |
| var location = _location; |
| if (location != null && _isFileUri(location)) { |
| pubspecPath = path.fromUri(_location); |
| } |
| |
| return _sources![sourceName]!.parseRef( |
| name, |
| descriptionNode?.value, |
| containingPath: pubspecPath, |
| languageVersion: languageVersion, |
| ); |
| }, targetPackage: name); |
| |
| dependencies[name] = |
| ref.withConstraint(versionConstraint).withFeatures(features); |
| }); |
| |
| return dependencies; |
| } |
| |
| /// Parses [node] to a [VersionConstraint]. |
| /// |
| /// If or [defaultUpperBoundConstraint] is specified then it will be set as |
| /// the max constraint if the original constraint doesn't have an upper |
| /// bound and it is compatible with [defaultUpperBoundConstraint]. |
| /// |
| /// If [ignoreUpperBound] the max constraint is ignored. |
| VersionConstraint _parseVersionConstraint(YamlNode? node, |
| {VersionConstraint? defaultUpperBoundConstraint, |
| bool ignoreUpperBound = false}) { |
| if (node?.value == null) { |
| return defaultUpperBoundConstraint ?? VersionConstraint.any; |
| } |
| if (node!.value is! String) { |
| _error('A version constraint must be a string.', node.span); |
| } |
| |
| return _wrapFormatException('version constraint', node.span, () { |
| var constraint = VersionConstraint.parse(node.value); |
| if (defaultUpperBoundConstraint != null && |
| constraint is VersionRange && |
| constraint.max == null && |
| defaultUpperBoundConstraint.allowsAny(constraint)) { |
| constraint = VersionConstraint.intersection( |
| [constraint, defaultUpperBoundConstraint]); |
| } |
| if (ignoreUpperBound && constraint is VersionRange) { |
| return VersionRange( |
| min: constraint.min, includeMin: constraint.includeMin); |
| } |
| return constraint; |
| }); |
| } |
| |
| /// Parses [node] to a map from feature names to whether those features are |
| /// enabled. |
| Map<String, FeatureDependency> _parseDependencyFeatures(YamlNode? node) { |
| if (node?.value == null) return const {}; |
| if (node is! YamlMap) _error('Features must be a map.', node!.span); |
| |
| return mapMap(node.nodes, |
| key: (dynamic nameNode, dynamic _) => _validateFeatureName(nameNode), |
| value: (dynamic _, dynamic valueNode) { |
| var value = valueNode.value; |
| if (value is bool) { |
| return value |
| ? FeatureDependency.required |
| : FeatureDependency.unused; |
| } else if (value is String && value == 'if available') { |
| return FeatureDependency.ifAvailable; |
| } else { |
| _error('Features must be true, false, or "if available".', |
| valueNode.span); |
| } |
| }); |
| } |
| |
| /// Verifies that [node] is a string and a valid feature name, and returns it |
| /// if so. |
| String _validateFeatureName(YamlNode node) { |
| var name = node.value; |
| if (name is! String) { |
| _error('A feature name must be a string.', node.span); |
| } else if (!packageNameRegExp.hasMatch(name)) { |
| _error('A feature name must be a valid Dart identifier.', node.span); |
| } |
| |
| return name; |
| } |
| |
| /// Verifies that [node] is a list of strings and returns it. |
| /// |
| /// If [validate] is passed, it's called for each string in [node]. |
| List<String> _parseStringList(YamlNode? node, |
| {void Function(String value, SourceSpan)? validate}) { |
| var list = _parseList(node); |
| for (var element in list.nodes) { |
| var value = element.value; |
| if (value is String) { |
| if (validate != null) validate(value, element.span); |
| } else { |
| _error('Must be a string.', element.span); |
| } |
| } |
| return list.cast<String>(); |
| } |
| |
| /// Verifies that [node] is a list and returns it. |
| YamlList _parseList(YamlNode? node) { |
| if (node == null || node.value == null) return YamlList(); |
| if (node is YamlList) return node; |
| _error('Must be a list.', node.span); |
| } |
| |
| /// Runs [fn] and wraps any [FormatException] it throws in a |
| /// [PubspecException]. |
| /// |
| /// [description] should be a noun phrase that describes whatever's being |
| /// parsed or processed by [fn]. [span] should be the location of whatever's |
| /// being processed within the pubspec. |
| /// |
| /// If [targetPackage] is provided, the value is used to describe the |
| /// dependency that caused the problem. |
| T _wrapFormatException<T>( |
| String description, SourceSpan? span, T Function() fn, |
| {String? targetPackage}) { |
| try { |
| return fn(); |
| } on FormatException catch (e) { |
| // If we already have a pub exception with a span, re-use that |
| if (e is PubspecException) rethrow; |
| |
| var msg = 'Invalid $description'; |
| if (targetPackage != null) { |
| msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency'; |
| } |
| msg = '$msg: ${e.message}'; |
| _error(msg, span); |
| } |
| } |
| |
| /// Throws a [PubspecException] with the given message. |
| Never _error(String message, SourceSpan? span) { |
| throw PubspecException(message, span); |
| } |
| } |
| |
| /// Returns whether [uri] is a file URI. |
| /// |
| /// This is slightly more complicated than just checking if the scheme is |
| /// 'file', since relative URIs also refer to the filesystem on the VM. |
| bool _isFileUri(Uri uri) => uri.scheme == 'file' || uri.scheme == ''; |