Add support for pubspec overrides file (#3215)
This change adds support for a new file pubspec_overrides.yaml to override parts of pubspec.yaml. Overrides are only active for the get and upgrade commands.
An overrides file has the same structure as pubspec.yaml, but only supports a subset of its fields. Currently, only the following top-level field override pubspec:
dependency_overrides
All other fields in the overrides file causes an error.
A dependency_overrides, in the overrides file, completely replace a dependency_overrides in pubspec.yaml. The two files are merged in a way that preserves source references to provide correct error messages.
When overrides are active, a warning is logged.
Fixes #2161 .
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 5a2a50f..5f4163f 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -145,6 +145,14 @@
/// The path to the entrypoint package's pubspec.
String get pubspecPath => p.normalize(root.path('pubspec.yaml'));
+ /// Whether the entrypoint package contains a `pubspec_overrides.yaml` file.
+ bool get hasPubspecOverrides =>
+ !root.isInMemory && fileExists(pubspecOverridesPath);
+
+ /// The path to the entrypoint package's pubspec overrides file.
+ String get pubspecOverridesPath =>
+ p.normalize(root.path('pubspec_overrides.yaml'));
+
/// The path to the entrypoint package's lockfile.
String get lockFilePath => p.normalize(p.join(_configRoot!, 'pubspec.lock'));
@@ -178,7 +186,8 @@
Entrypoint(
String rootDir,
this.cache,
- ) : root = Package.load(null, rootDir, cache.sources),
+ ) : root = Package.load(null, rootDir, cache.sources,
+ withPubspecOverrides: true),
globalDir = null;
Entrypoint.inMemory(this.root, this.cache,
@@ -260,6 +269,11 @@
required PubAnalytics? analytics,
bool onlyReportSuccessOrFailure = false,
}) async {
+ if (!onlyReportSuccessOrFailure && hasPubspecOverrides) {
+ log.warning(
+ 'Warning: pubspec.yaml has overrides from $pubspecOverridesPath');
+ }
+
final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
SolveResult result;
try {
@@ -513,11 +527,22 @@
var pubspecModified = File(pubspecPath).lastModifiedSync();
var lockFileModified = File(lockFilePath).lastModifiedSync();
+ var pubspecChanged = lockFileModified.isBefore(pubspecModified);
+ var pubspecOverridesChanged = false;
+
+ if (hasPubspecOverrides) {
+ var pubspecOverridesModified =
+ File(pubspecOverridesPath).lastModifiedSync();
+ pubspecOverridesChanged =
+ lockFileModified.isBefore(pubspecOverridesModified);
+ }
+
var touchedLockFile = false;
- if (lockFileModified.isBefore(pubspecModified) || hasPathDependencies) {
- // If `pubspec.lock` is newer than `pubspec.yaml` or we have path
- // dependencies, then we check that `pubspec.lock` is a correct solution
- // for the requirements in `pubspec.yaml`. This aims to:
+ if (pubspecChanged || pubspecOverridesChanged || hasPathDependencies) {
+ // If `pubspec.lock` is older than `pubspec.yaml` or
+ // `pubspec_overrides.yaml`, or we have path dependencies, then we check
+ // that `pubspec.lock` is a correct solution for the requirements in
+ // `pubspec.yaml` and `pubspec_overrides.yaml`. This aims to:
// * Prevent missing packages when `pubspec.lock` is checked into git.
// * Mitigate missing transitive dependencies when the `pubspec.yaml` in
// a path dependency is changed.
@@ -526,7 +551,8 @@
touchedLockFile = true;
touch(lockFilePath);
} else {
- dataError('The $pubspecPath file has changed since the $lockFilePath '
+ var filePath = pubspecChanged ? pubspecPath : pubspecOverridesPath;
+ dataError('The $filePath file has changed since the $lockFilePath '
'file was generated, please run "$topLevelProgram pub get" again.');
}
}
@@ -544,11 +570,11 @@
var packageConfigModified = File(packageConfigFile).lastModifiedSync();
if (packageConfigModified.isBefore(lockFileModified) ||
hasPathDependencies) {
- // If `package_config.json` is newer than `pubspec.lock` or we have
+ // If `package_config.json` is older than `pubspec.lock` or we have
// path dependencies, then we check that `package_config.json` is a
// correct configuration on the local machine. This aims to:
// * Mitigate issues when copying a folder from one machine to another.
- // * Force `pub get` if a path dependency has changed language verison.
+ // * Force `pub get` if a path dependency has changed language version.
_checkPackageConfigUpToDate();
touch(packageConfigFile);
} else if (touchedLockFile) {
diff --git a/lib/src/package.dart b/lib/src/package.dart
index f787678..c004098 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -68,7 +68,8 @@
/// The immediate dev dependencies this package specifies in its pubspec.
Map<String, PackageRange> get devDependencies => pubspec.devDependencies;
- /// The dependency overrides this package specifies in its pubspec.
+ /// The dependency overrides this package specifies in its pubspec or pubspec
+ /// overrides.
Map<String, PackageRange> get dependencyOverrides =>
pubspec.dependencyOverrides;
@@ -147,8 +148,24 @@
/// [name] is the expected name of that package (e.g. the name given in the
/// dependency), or `null` if the package being loaded is the entrypoint
/// package.
- Package.load(String? name, String this._dir, SourceRegistry sources)
- : pubspec = Pubspec.load(_dir, sources, expectedName: name);
+ ///
+ /// `pubspec_overrides.yaml` is only loaded if [withPubspecOverrides] is
+ /// `true`.
+ factory Package.load(
+ String? name,
+ String dir,
+ SourceRegistry sources, {
+ bool withPubspecOverrides = false,
+ }) {
+ final pubspec = Pubspec.load(dir, sources,
+ expectedName: name, allowOverridesFile: withPubspecOverrides);
+ return Package._(dir, pubspec);
+ }
+
+ Package._(
+ this._dir,
+ this.pubspec,
+ );
/// Constructs a package with the given pubspec.
///
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index cc75b2d..bce47e1 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -69,12 +69,27 @@
// initialization can throw a [PubspecException], that error should also be
// exposed through [allErrors].
+ /// The fields of [pubspecOverridesFilename]. `null` if no such file exists or has
+ /// to be considered.
+ final YamlMap? _overridesFileFields;
+
+ String? get _packageName => fields['name'] != null ? name : null;
+
+ /// The name of the manifest file.
+ static const pubspecYamlFilename = 'pubspec.yaml';
+
+ /// The filename of the pubspec overrides file.
+ ///
+ /// This file can contain dependency_overrides that override those in
+ /// pubspec.yaml.
+ static const pubspecOverridesFilename = 'pubspec_overrides.yaml';
+
/// 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;
+ final SourceRegistry _sources;
/// The location from which the pubspec was loaded.
///
@@ -83,14 +98,27 @@
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> get dependencies =>
+ _dependencies ??= _parseDependencies(
+ 'dependencies',
+ fields.nodes['dependencies'],
+ _sources,
+ languageVersion,
+ _packageName,
+ _location);
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> get devDependencies =>
+ _devDependencies ??= _parseDependencies(
+ 'dev_dependencies',
+ fields.nodes['dev_dependencies'],
+ _sources,
+ languageVersion,
+ _packageName,
+ _location,
+ );
Map<String, PackageRange>? _devDependencies;
@@ -99,9 +127,41 @@
///
/// 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']);
+ ///
+ /// These can occur both in the pubspec.yaml file and the [pubspecOverridesFilename].
+ Map<String, PackageRange> get dependencyOverrides {
+ if (_dependencyOverrides != null) return _dependencyOverrides!;
+ final pubspecOverridesFields = _overridesFileFields;
+ if (pubspecOverridesFields != null) {
+ pubspecOverridesFields.nodes.forEach((key, _) {
+ if (!const {'dependency_overrides'}.contains(key.value)) {
+ throw PubspecException(
+ 'pubspec_overrides.yaml only supports the `dependency_overrides` field.',
+ key.span,
+ );
+ }
+ });
+ if (pubspecOverridesFields.containsKey('dependency_overrides')) {
+ _dependencyOverrides = _parseDependencies(
+ 'dependency_overrides',
+ pubspecOverridesFields.nodes['dependency_overrides'],
+ _sources,
+ languageVersion,
+ _packageName,
+ _location,
+ fileType: _FileType.pubspecOverrides,
+ );
+ }
+ }
+ return _dependencyOverrides ??= _parseDependencies(
+ 'dependency_overrides',
+ fields.nodes['dependency_overrides'],
+ _sources,
+ languageVersion,
+ _packageName,
+ _location,
+ );
+ }
Map<String, PackageRange>? _dependencyOverrides;
@@ -140,7 +200,13 @@
});
var dependencies = _parseDependencies(
- 'dependencies', specNode.nodes['dependencies']);
+ 'dependencies',
+ specNode.nodes['dependencies'],
+ _sources,
+ languageVersion,
+ _packageName,
+ _location,
+ );
var sdkConstraints = _parseEnvironment(specNode);
@@ -250,7 +316,8 @@
}
var constraints = {
- 'dart': _parseVersionConstraint(yaml.nodes['sdk'],
+ 'dart': _parseVersionConstraint(
+ yaml.nodes['sdk'], _packageName, _FileType.pubspec,
defaultUpperBoundConstraint: _includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
: null)
@@ -263,10 +330,11 @@
}
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');
+ constraints[name.value as String] =
+ _parseVersionConstraint(constraint, _packageName, _FileType.pubspec,
+ // Flutter constraints get special treatment, as Flutter won't be
+ // using semantic versioning to mark breaking releases.
+ ignoreUpperBound: name.value == 'flutter');
});
return constraints;
@@ -280,10 +348,14 @@
///
/// If [expectedName] is passed and the pubspec doesn't have a matching name
/// field, this will throw a [PubspecException].
+ ///
+ /// If [allowOverridesFile] is `true` [pubspecOverridesFilename] is loaded and
+ /// is allowed to override dependency_overrides from `pubspec.yaml`.
factory Pubspec.load(String packageDir, SourceRegistry sources,
- {String? expectedName}) {
- var pubspecPath = path.join(packageDir, 'pubspec.yaml');
- var pubspecUri = path.toUri(pubspecPath);
+ {String? expectedName, bool allowOverridesFile = false}) {
+ var pubspecPath = path.join(packageDir, pubspecYamlFilename);
+ var overridesPath = path.join(packageDir, pubspecOverridesFilename);
+
if (!fileExists(pubspecPath)) {
throw FileException(
// Make the package dir absolute because for the entrypoint it'll just
@@ -292,20 +364,31 @@
'"${canonicalize(packageDir)}".',
pubspecPath);
}
+ String? overridesFileContents =
+ allowOverridesFile && fileExists(overridesPath)
+ ? readTextFile(overridesPath)
+ : null;
- return Pubspec.parse(readTextFile(pubspecPath), sources,
- expectedName: expectedName, location: pubspecUri);
+ return Pubspec.parse(
+ readTextFile(pubspecPath),
+ sources,
+ expectedName: expectedName,
+ location: path.toUri(pubspecPath),
+ overridesFileContents: overridesFileContents,
+ overridesLocation: path.toUri(overridesPath),
+ );
}
- 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
+ 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
@@ -317,22 +400,13 @@
_sdkConstraints = sdkConstraints ??
UnmodifiableMapView({'dart': VersionConstraint.any}),
_includeDefaultSdkConstraint = false,
- _sources = sources,
+ _sources = sources ?? SourceRegistry(),
+ _overridesFileFields = null,
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.
@@ -342,8 +416,9 @@
///
/// [location] is the location from which this pubspec was loaded.
Pubspec.fromMap(Map fields, this._sources,
- {String? expectedName, Uri? location})
- : _includeDefaultSdkConstraint = true,
+ {YamlMap? overridesFields, String? expectedName, Uri? location})
+ : _overridesFileFields = overridesFields,
+ _includeDefaultSdkConstraint = true,
super(fields is YamlMap
? fields
: YamlMap.wrap(fields, sourceUrl: location)) {
@@ -358,31 +433,49 @@
this.fields.nodes['name']!.span);
}
- /// Parses the pubspec stored at [filePath] whose text is [contents].
+ /// Parses the pubspec stored at [location] 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;
+ factory Pubspec.parse(
+ String contents,
+ SourceRegistry sources, {
+ String? expectedName,
+ Uri? location,
+ String? overridesFileContents,
+ Uri? overridesLocation,
+ }) {
+ late final YamlMap pubspecMap;
+ YamlMap? overridesFileMap;
try {
- pubspecNode = loadYamlNode(contents, sourceUrl: location);
+ pubspecMap = _ensureMap(loadYamlNode(contents, sourceUrl: location));
+ if (overridesFileContents != null) {
+ overridesFileMap = _ensureMap(
+ loadYamlNode(overridesFileContents, sourceUrl: overridesLocation));
+ }
} 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);
+ overridesFields: overridesFileMap,
+ expectedName: expectedName,
+ location: location);
+ }
+
+ /// Ensures that [node] is a mapping.
+ ///
+ /// If [node] is already a map it is returned.
+ /// If [node] is yaml-null an empty map is returned.
+ /// Otherwise an exception is thrown.
+ static YamlMap _ensureMap(YamlNode node) {
+ if (node is YamlScalar && node.value == null) {
+ return YamlMap(sourceUrl: node.span.sourceUrl);
+ } else if (node is YamlMap) {
+ return node;
+ } else {
+ throw PubspecException('The pubspec must be a YAML mapping.', node.span);
+ }
}
/// Returns a list of most errors in this pubspec.
@@ -409,29 +502,39 @@
_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>{};
+/// Parses the dependency field named [field], and returns the corresponding
+/// map of dependency names to dependencies.
+Map<String, PackageRange> _parseDependencies(
+ String field,
+ YamlNode? node,
+ SourceRegistry sources,
+ LanguageVersion languageVersion,
+ String? packageName,
+ Uri? location, {
+ _FileType fileType = _FileType.pubspec,
+}) {
+ var dependencies = <String, PackageRange>{};
- // Allow an empty dependencies key.
- if (node == null || node.value == null) return dependencies;
+ // 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);
- }
+ 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);
- }
+ 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) {
+ node.nodes.forEach(
+ (nameNode, specNode) {
var name = nameNode.value;
var spec = specNode.value;
- if (fields['name'] != null && name == this.name) {
+ if (packageName != null && name == packageName) {
_error('A package may not list itself as a dependency.', nameNode.span);
}
@@ -441,10 +544,11 @@
VersionConstraint versionConstraint = VersionRange();
var features = const <String, FeatureDependency>{};
if (spec == null) {
- sourceName = _sources!.defaultSource.name;
+ sourceName = sources.defaultSource.name;
} else if (spec is String) {
- sourceName = _sources!.defaultSource.name;
- versionConstraint = _parseVersionConstraint(specNode);
+ sourceName = sources.defaultSource.name;
+ versionConstraint =
+ _parseVersionConstraint(specNode, packageName, fileType);
} else if (spec is Map) {
// Don't write to the immutable YAML map.
spec = Map.from(spec);
@@ -452,7 +556,11 @@
if (spec.containsKey('version')) {
spec.remove('version');
- versionConstraint = _parseVersionConstraint(specMap.nodes['version']);
+ versionConstraint = _parseVersionConstraint(
+ specMap.nodes['version'],
+ packageName,
+ fileType,
+ );
}
if (spec.containsKey('features')) {
@@ -483,151 +591,82 @@
// 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);
+ pubspecPath = path.fromUri(location);
}
- return _sources![sourceName]!.parseRef(
+ return sources[sourceName]!.parseRef(
name,
descriptionNode?.value,
containingPath: pubspecPath,
languageVersion: languageVersion,
);
- }, targetPackage: name);
+ }, packageName, fileType, targetPackage: name);
dependencies[name] =
ref.withConstraint(versionConstraint).withFeatures(features);
- });
+ },
+ );
- return dependencies;
+ return dependencies;
+}
+
+/// 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);
}
- /// 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 name;
+}
- 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);
+/// 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>();
+}
- /// Throws a [PubspecException] with the given message.
- Never _error(String message, SourceSpan? span) {
- throw PubspecException(message, span);
- }
+/// 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);
}
/// Returns whether [uri] is a file URI.
@@ -635,3 +674,86 @@
/// 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 == '';
+
+/// 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, String? packageName, _FileType fileType,
+ {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;
+ }, packageName, fileType);
+}
+
+/// 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? packageName, _FileType fileType,
+ {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';
+ final typeName = _fileTypeName(fileType);
+ if (targetPackage != null) {
+ msg = '$msg in the "$packageName" $typeName 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);
+}
+
+enum _FileType {
+ pubspec,
+ pubspecOverrides,
+}
+
+String _fileTypeName(_FileType type) {
+ switch (type) {
+ case _FileType.pubspec:
+ return 'pubspec';
+ case _FileType.pubspecOverrides:
+ return 'pubspec override';
+ }
+}
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart
index 8e1abed..58808f5 100644
--- a/lib/src/solver/package_lister.dart
+++ b/lib/src/solver/package_lister.dart
@@ -49,7 +49,7 @@
/// The type of the dependency from the root package onto [_ref].
final DependencyType _dependencyType;
- /// The set of package names that were overridden by the root package.
+ /// The set of packages that were overridden by the root package.
final Set<String> _overriddenPackages;
/// Whether this is a downgrade, in which case the package priority should be
@@ -118,7 +118,8 @@
// package.
_locked = PackageId.root(package),
_dependencyType = DependencyType.none,
- _overriddenPackages = const UnmodifiableSetView.empty(),
+ _overriddenPackages =
+ Set.unmodifiable(package.dependencyOverrides.keys),
_isDowngrade = false,
_allowedRetractedVersion = null;
@@ -227,12 +228,12 @@
var incompatibilities = <Incompatibility>[];
for (var range in pubspec.dependencies.values) {
- if (pubspec.dependencyOverrides.containsKey(range.name)) continue;
+ if (_overriddenPackages.contains(range.name)) continue;
incompatibilities.add(_dependency(depender, range));
}
for (var range in pubspec.devDependencies.values) {
- if (pubspec.dependencyOverrides.containsKey(range.name)) continue;
+ if (_overriddenPackages.contains(range.name)) continue;
incompatibilities.add(_dependency(depender, range));
}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index d4e3a75..c531440 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:math' as math;
-import 'package:collection/collection.dart';
import 'package:pub_semver/pub_semver.dart';
import '../exceptions.dart';
@@ -80,7 +79,7 @@
VersionSolver(this._type, this._systemCache, this._root, this._lockFile,
Iterable<String> unlock)
- : _dependencyOverrides = _root.pubspec.dependencyOverrides,
+ : _dependencyOverrides = _root.dependencyOverrides,
_unlock = {...unlock};
/// Finds a set of dependencies that match the root package's constraints, or
@@ -471,12 +470,12 @@
var locked = _getLocked(ref.name);
if (locked != null && !locked.samePackage(ref)) locked = null;
- Set<String> overridden = MapKeySet(_dependencyOverrides);
- if (overridden.contains(package.name)) {
+ final overridden = <String>{
+ ..._dependencyOverrides.keys,
// If the package is overridden, ignore its dependencies back onto the
// root package.
- overridden = Set.from(overridden)..add(_root.name);
- }
+ if (_dependencyOverrides.containsKey(package.name)) _root.name
+ };
return PackageLister(
_systemCache,
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 7b140e2..2491df4 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -107,6 +107,16 @@
return pubspec(map);
}
+/// Describes a file named `pubspec_overrides.yaml` by default, with the given
+/// YAML-serialized [contents], which should be a serializable object.
+///
+/// [contents] may contain [Future]s that resolve to serializable objects,
+/// which may in turn contain [Future]s recursively.
+Descriptor pubspecOverrides(Map<String, Object> contents) => YamlDescriptor(
+ 'pubspec_overrides.yaml',
+ yaml(contents),
+ );
+
/// Describes a directory named `lib` containing a single dart file named
/// `<name>.dart` that contains a line of Dart code.
Descriptor libDir(String name, [String? code]) {
diff --git a/test/pubspec_overrides_test.dart b/test/pubspec_overrides_test.dart
new file mode 100644
index 0000000..2e9234f
--- /dev/null
+++ b/test/pubspec_overrides_test.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2022, 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 'package:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+ forBothPubGetAndUpgrade((command) {
+ test('pubspec overrides', () async {
+ await servePackages()
+ ..serve('lib', '1.0.0')
+ ..serve('lib', '2.0.0');
+
+ await d.dir(appPath, [
+ d.appPubspec({'lib': '1.0.0'}),
+ d.dir('lib'),
+ d.pubspecOverrides({
+ 'dependency_overrides': {'lib': '2.0.0'}
+ }),
+ ]).create();
+
+ await pubCommand(
+ command,
+ warning:
+ 'Warning: pubspec.yaml has overrides from pubspec_overrides.yaml\n'
+ 'Warning: You are using these overridden dependencies:\n'
+ '! lib 2.0.0',
+ );
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'lib',
+ version: '2.0.0',
+ languageVersion: '2.7',
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ languageVersion: '0.1',
+ ),
+ ])
+ ]).validate();
+ });
+ });
+}
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index 62f77c4..b149a9c 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -2,6 +2,8 @@
// 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:pub/src/language_version.dart';
import 'package:pub/src/package_name.dart';
import 'package:pub/src/pubspec.dart';
@@ -186,7 +188,7 @@
expect(foo.source, equals(sources['hosted']));
});
- test('throws if it dependes on itself', () {
+ test('throws if it depends on itself', () {
expectPubspecException('''
name: myapp
dependencies:
@@ -817,5 +819,89 @@
});
});
});
+
+ group('pubspec overrides', () {
+ Pubspec parsePubspecOverrides(String overridesContents) {
+ return Pubspec.parse(
+ '''
+name: app
+environment:
+ sdk: '>=2.7.0 <3.0.0'
+dependency_overrides:
+ bar: 2.1.0
+''',
+ sources,
+ overridesFileContents: overridesContents,
+ overridesLocation: Uri.parse('file:///pubspec_overrides.yaml'),
+ );
+ }
+
+ void expectPubspecOverridesException(
+ String contents,
+ void Function(Pubspec) fn, [
+ String? expectedContains,
+ ]) {
+ var expectation = isA<PubspecException>();
+ if (expectedContains != null) {
+ expectation = expectation.having((error) => error.toString(),
+ 'toString()', contains(expectedContains));
+ }
+
+ var pubspec = parsePubspecOverrides(contents);
+ expect(() => fn(pubspec), throwsA(expectation));
+ }
+
+ test('allows empty overrides file', () {
+ var pubspec = parsePubspecOverrides('');
+ expect(pubspec.dependencyOverrides['foo'], isNull);
+ final bar = pubspec.dependencyOverrides['bar']!;
+ expect(bar.name, equals('bar'));
+ expect(bar.source, equals(sources['hosted']));
+ expect(bar.constraint, VersionConstraint.parse('2.1.0'));
+ });
+
+ test('allows empty dependency_overrides section', () {
+ final pubspec = parsePubspecOverrides('''
+dependency_overrides:
+''');
+ expect(pubspec.dependencyOverrides, isEmpty);
+ });
+
+ test('parses dependencies in dependency_overrides section', () {
+ final pubspec = parsePubspecOverrides('''
+dependency_overrides:
+ foo:
+ version: 1.0.0
+''');
+
+ expect(pubspec.dependencyOverrides['bar'], isNull);
+
+ final foo = pubspec.dependencyOverrides['foo']!;
+ expect(foo.name, equals('foo'));
+ expect(foo.source, equals(sources['hosted']));
+ expect(foo.constraint, VersionConstraint.parse('1.0.0'));
+ });
+
+ test('throws exception with correct source references', () {
+ expectPubspecOverridesException('''
+dependency_overrides:
+ foo:
+ fake: bad
+''', (pubspecOverrides) => pubspecOverrides.dependencyOverrides,
+ 'Error on line 3, column 11 of ${Platform.pathSeparator}pubspec_overrides.yaml');
+ });
+
+ test('throws if overrides contain invalid dependency section', () {
+ expectPubspecOverridesException('''
+dependency_overrides: false
+''', (pubspecOverrides) => pubspecOverrides.dependencyOverrides);
+ });
+
+ test('throws if overrides contain an unknown field', () {
+ expectPubspecOverridesException('''
+name: 'foo'
+''', (pubspecOverrides) => pubspecOverrides.dependencyOverrides);
+ });
+ });
});
}
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index d36a16f..e3ec30e 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -1926,6 +1926,44 @@
await expectResolves(result: {'foo': '1.2.3', 'bar': '0.0.1'});
});
+
+ test('overrides in pubspec_overrides.yaml', () async {
+ await servePackages()
+ ..serve('a', '1.0.0')
+ ..serve('a', '2.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'a': '1.0.0'},
+ }),
+ d.pubspecOverrides({
+ 'dependency_overrides': {'a': '2.0.0'}
+ }),
+ ]).create();
+
+ await expectResolves(result: {'a': '2.0.0'});
+ });
+
+ test('pubspec_overrides.yaml takes precedence over pubspec.yaml', () async {
+ await servePackages()
+ ..serve('a', '1.0.0')
+ ..serve('a', '2.0.0')
+ ..serve('a', '3.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'a': '1.0.0'},
+ 'dependency_overrides': {'a': '2.0.0'}
+ }),
+ d.pubspecOverrides({
+ 'dependency_overrides': {'a': '3.0.0'}
+ }),
+ ]).create();
+
+ await expectResolves(result: {'a': '3.0.0'});
+ });
}
void downgrade() {