diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index 31d4a1159..49352e791 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart @@ -117,6 +117,11 @@ For example (follow the same format including spaces): help: 'Path of git package in repository', hide: true, ); + argParser.addOption( + 'git-tag-pattern', + help: 'The tag-pattern to search for versions in repository', + hide: true, + ); argParser.addOption( 'hosted-url', help: 'URL of package host server', @@ -275,7 +280,9 @@ Specify multiple sdk packages with descriptors.'''); location: Uri.parse(entrypoint.workPackage.pubspecPath), overridesFileContents: overridesFileContents, overridesLocation: Uri.file(overridesPath), - containingDescription: RootDescription(entrypoint.workPackage.dir), + containingDescription: ResolvedRootDescription.fromDir( + entrypoint.workPackage.dir, + ), ), ) .acquireDependencies( @@ -541,6 +548,11 @@ Specify multiple sdk packages with descriptors.'''); if (gitUrl == null) { usageException('The `--git-url` is required for git dependencies.'); } + if (argResults.gitRef != null && argResults.tagPattern != null) { + usageException( + 'Cannot provide both `--git-ref` and `--git-tag-pattern`.', + ); + } /// Process the git options to return the simplest representation to be /// added to the pubspec. @@ -552,6 +564,7 @@ Specify multiple sdk packages with descriptors.'''); containingDir: p.current, ref: argResults.gitRef, path: argResults.gitPath, + tagPattern: argResults.tagPattern, ), ); } on FormatException catch (e) { @@ -566,7 +579,7 @@ Specify multiple sdk packages with descriptors.'''); ref = cache.sdk.parseRef( packageName, argResults.sdk, - containingDescription: RootDescription(p.current), + containingDescription: ResolvedRootDescription.fromDir(p.current), ); } else { ref = PackageRef( @@ -652,7 +665,7 @@ Specify multiple sdk packages with descriptors.'''); cache.sources, // Resolve relative paths relative to current, not where the // pubspec.yaml is. - containingDescription: RootDescription(p.current), + containingDescription: ResolvedRootDescription.fromDir(p.current), ); } on FormatException catch (e) { usageException('Failed parsing package specification: ${e.message}'); @@ -787,6 +800,8 @@ extension on ArgResults { bool get isDryRun => flag('dry-run'); String? get gitUrl => this['git-url'] as String?; String? get gitPath => this['git-path'] as String?; + String? get tagPattern => this['git-tag-pattern'] as String?; + String? get gitRef => this['git-ref'] as String?; String? get hostedUrl => this['hosted-url'] as String?; String? get path => this['path'] as String?; diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart index fddbb1c33..9b8c63acb 100644 --- a/lib/src/command/dependency_services.dart +++ b/lib/src/command/dependency_services.dart @@ -424,6 +424,7 @@ class DependencyServicesApplyCommand extends PubCommand { } else if (targetRevision != null && (lockFileYaml['packages'] as Map).containsKey(targetPackage)) { final ref = entrypoint.lockFile.packages[targetPackage]!.toRef(); + final currentDescription = ref.description as GitDescription; final updatedRef = PackageRef( targetPackage, @@ -432,6 +433,7 @@ class DependencyServicesApplyCommand extends PubCommand { path: currentDescription.path, ref: targetRevision, containingDir: directory, + tagPattern: currentDescription.tagPattern, ), ); final versions = await cache.getVersions(updatedRef); @@ -480,7 +482,9 @@ class DependencyServicesApplyCommand extends PubCommand { updatedPubspecs[package.dir].toString(), cache.sources, location: toUri(package.pubspecPath), - containingDescription: RootDescription(package.dir), + containingDescription: ResolvedRootDescription( + RootDescription(package.dir), + ), ), ); // Resolve versions, this will update transitive dependencies that were diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 804287aab..a2d405f87 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -395,7 +395,9 @@ the \$PUB_HOSTED_URL environment variable.'''); ), ), cache.sources, - containingDescription: RootDescription(p.dirname(archive)), + containingDescription: ResolvedRootDescription.fromDir( + p.dirname(archive), + ), ); } on FormatException catch (e) { dataError('Failed to read pubspec.yaml from archive: ${e.message}'); diff --git a/lib/src/command/unpack.dart b/lib/src/command/unpack.dart index 21d096be9..98821205a 100644 --- a/lib/src/command/unpack.dart +++ b/lib/src/command/unpack.dart @@ -144,7 +144,9 @@ in a directory `foo-`. final pubspec = Pubspec.load( destinationDir, cache.sources, - containingDescription: RootDescription(destinationDir), + containingDescription: ResolvedRootDescription.fromDir( + destinationDir, + ), ); final buffer = StringBuffer(); if (pubspec.resolution != Resolution.none) { @@ -212,7 +214,7 @@ Creating `pubspec_overrides.yaml` to resolve it without those overrides.'''); // Resolve relative paths relative to current, not where the // pubspec.yaml is. location: p.toUri(p.join(p.current, 'descriptor')), - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); } on FormatException catch (e) { usageException('Failed parsing package specification: ${e.message}'); diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 5626dde41..9d4ba459a 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart @@ -17,7 +17,6 @@ import '../package_name.dart'; import '../pubspec.dart'; import '../pubspec_utils.dart'; import '../solver.dart'; -import '../source/hosted.dart'; import '../utils.dart'; /// Handles the `upgrade` pub command. @@ -248,11 +247,11 @@ be direct 'dependencies' or 'dev_dependencies', following packages are not: // Mapping from original to changed value. var changes = >{}; for (final package in entrypoint.workspaceRoot.transitiveWorkspace) { - final declaredHostedDependencies = [ + final declaredUpgradableDependencies = [ ...package.dependencies.values, ...package.devDependencies.values, - ].where((dep) => dep.source is HostedSource); - for (final dep in declaredHostedDependencies) { + ].where((dep) => dep.description.hasMultipleVersions); + for (final dep in declaredUpgradableDependencies) { final resolvedPackage = resolvedPackages[dep.name]!; if (!toUpgrade.contains(dep.name)) { // If we're not trying to upgrade this package, or it wasn't in the diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 36719a185..de21237e5 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart @@ -97,7 +97,7 @@ class Entrypoint { pubspec = Pubspec.load( dir, cache.sources, - containingDescription: RootDescription(dir), + containingDescription: ResolvedRootDescription.fromDir(dir), allowOverridesFile: true, ); } on FileException { @@ -116,7 +116,9 @@ class Entrypoint { cache.sources, expectedName: expectedName, allowOverridesFile: withPubspecOverrides, - containingDescription: RootDescription(path), + containingDescription: ResolvedRootDescription.fromDir( + path, + ), ), withPubspecOverrides: true, ); diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index 2d4eae346..880c7f9ee 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart @@ -93,6 +93,7 @@ class GlobalPackages { required bool overwriteBinStubs, String? path, String? ref, + String? tagPattern, }) async { final name = await cache.git.getPackageNameFromRepo( repo, @@ -100,6 +101,7 @@ class GlobalPackages { path, cache, relativeTo: p.current, + tagPattern: tagPattern, ); // TODO(nweiz): Add some special handling for git repos that contain path @@ -115,7 +117,7 @@ class GlobalPackages { if (path != null) 'path': path, if (ref != null) 'ref': ref, }, - containingDescription: RootDescription(p.current), + containingDescription: ResolvedRootDescription.fromDir(p.current), languageVersion: LanguageVersion.fromVersion(sdk.version), ); } on FormatException catch (e) { diff --git a/lib/src/language_version.dart b/lib/src/language_version.dart index 6bf4c8adf..fdfb2bfb5 100644 --- a/lib/src/language_version.dart +++ b/lib/src/language_version.dart @@ -65,6 +65,8 @@ class LanguageVersion implements Comparable { bool get supportsWorkspaces => this >= firstVersionWithWorkspaces; + bool get supportsTagPattern => this >= firstVersionWithTagPattern; + bool get forbidsUnknownDescriptionKeys => this >= firstVersionForbidingUnknownDescriptionKeys; @@ -105,6 +107,7 @@ class LanguageVersion implements Comparable { static const firstVersionWithNullSafety = LanguageVersion(2, 12); static const firstVersionWithShorterHostedSyntax = LanguageVersion(2, 15); static const firstVersionWithWorkspaces = LanguageVersion(3, 5); + static const firstVersionWithTagPattern = LanguageVersion(3, 9); static const firstVersionForbidingUnknownDescriptionKeys = LanguageVersion( 3, 7, diff --git a/lib/src/package_name.dart b/lib/src/package_name.dart index 647561ed0..7ec1c285e 100644 --- a/lib/src/package_name.dart +++ b/lib/src/package_name.dart @@ -174,7 +174,7 @@ class PackageRange { bool get _showVersionConstraint { if (isRoot) return false; if (!constraint.isAny) return true; - return description.source.hasMultipleVersions; + return description.hasMultipleVersions; } /// Returns a copy of `this` with the same semantics, but with a `^`-style diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index 0923ba74d..5ebc55fa1 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart @@ -61,7 +61,7 @@ class Pubspec extends PubspecBase { /// It is used to resolve relative paths. And to resolve path-descriptions /// from a git dependency as git-descriptions. - final Description _containingDescription; + final ResolvedDescription _containingDescription; /// Directories of packages that should resolve together with this package. late List workspace = () { @@ -279,7 +279,7 @@ environment: SourceRegistry sources, { String? expectedName, bool allowOverridesFile = false, - required Description containingDescription, + required ResolvedDescription containingDescription, }) { final pubspecPath = p.join(packageDir, pubspecYamlFilename); final overridesPath = p.join(packageDir, pubspecOverridesFilename); @@ -324,7 +324,7 @@ environment: sources, expectedName: expectedName, allowOverridesFile: withPubspecOverrides, - containingDescription: RootDescription(dir), + containingDescription: ResolvedRootDescription.fromDir(dir), ); } @@ -362,7 +362,7 @@ environment: _overridesFileFields = null, // This is a dummy value. Dependencies should already be resolved, so we // never need to do relative resolutions. - _containingDescription = RootDescription('.'), + _containingDescription = ResolvedRootDescription.fromDir('.'), super( fields == null ? YamlMap() : YamlMap.wrap(fields), name: name, @@ -382,7 +382,7 @@ environment: YamlMap? overridesFields, String? expectedName, Uri? location, - required Description containingDescription, + required ResolvedDescription containingDescription, }) : _overridesFileFields = overridesFields, _includeDefaultSdkConstraint = true, _givenSdkConstraints = null, @@ -432,7 +432,7 @@ environment: Uri? location, String? overridesFileContents, Uri? overridesLocation, - required Description containingDescription, + required ResolvedDescription containingDescription, }) { final YamlMap pubspecMap; YamlMap? overridesFileMap; @@ -576,7 +576,7 @@ Map _parseDependencies( SourceRegistry sources, LanguageVersion languageVersion, String? packageName, - Description containingDescription, { + ResolvedDescription containingDescription, { _FileType fileType = _FileType.pubspec, }) { final dependencies = {}; diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index 165da9b19..0d0f119e7 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart @@ -586,7 +586,10 @@ class VersionSolver { // can't be downgraded. if (_type == SolveType.downgrade) { final locked = _lockFile.packages[package]; - if (locked != null && !locked.source.hasMultipleVersions) return locked; + if (locked != null && + !locked.description.description.hasMultipleVersions) { + return locked; + } } if (_unlock.isEmpty || _unlock.contains(package)) return null; diff --git a/lib/src/source.dart b/lib/src/source.dart index 0c3fe70cc..37727e5b7 100644 --- a/lib/src/source.dart +++ b/lib/src/source.dart @@ -51,12 +51,6 @@ abstract class Source { /// all sources. String get name; - /// Whether this source can choose between multiple versions of the same - /// package during version solving. - /// - /// Defaults to `false`. - bool get hasMultipleVersions => false; - /// Parses a [PackageRef] from a name and a user-provided [description]. /// /// When a [Pubspec] is parsed, it reads in the description for each @@ -81,7 +75,7 @@ abstract class Source { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, required LanguageVersion languageVersion, }); @@ -190,6 +184,11 @@ abstract class Source { /// with a version constraint. abstract class Description { Source get source; + + /// Whether the source can choose between multiple versions of this + /// package during version solving. + bool get hasMultipleVersions; + Object? serializeForPubspec({ required String? containingDir, required LanguageVersion languageVersion, diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart index 86dbc0a18..2cfa2a1ff 100644 --- a/lib/src/source/cached.dart +++ b/lib/src/source/cached.dart @@ -32,7 +32,7 @@ abstract class CachedSource extends Source { packageDir, cache.sources, expectedName: id.name, - containingDescription: id.description.description, + containingDescription: id.description, ); } diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart index 7bed62f63..af104f390 100644 --- a/lib/src/source/git.dart +++ b/lib/src/source/git.dart @@ -24,6 +24,8 @@ import 'cached.dart'; import 'path.dart'; import 'root.dart'; +typedef TaggedVersion = ({Version version, String commitId}); + /// A package source that gets packages from Git repos. class GitSource extends CachedSource { static GitSource instance = GitSource._(); @@ -37,12 +39,13 @@ class GitSource extends CachedSource { PackageRef parseRef( String name, Object? description, { - Description? containingDescription, + ResolvedDescription? containingDescription, required LanguageVersion languageVersion, }) { String url; String? ref; String? path; + String? tagPattern; if (description is String) { url = description; } else if (description is! Map) { @@ -77,16 +80,42 @@ class GitSource extends CachedSource { } path = descriptionPath; + // TODO: can we avoid relying on key presence? + if (description.containsKey('tag_pattern')) { + if (!languageVersion.supportsTagPattern) { + throw FormatException( + 'Using `git: {tagPattern: }` is only supported with a minimum SDK ' + 'constraint of ${LanguageVersion.firstVersionWithTagPattern}.', + ); + } + switch (description['tag_pattern']) { + case final String descriptionTagPattern: + tagPattern = descriptionTagPattern; + // Do an early compilation to validate the format. + compileTagPattern(tagPattern); + default: + throw const FormatException( + "The 'tag_pattern' field of the description " + 'must be a string or null.', + ); + } + } + + if (ref != null && tagPattern != null) { + throw const FormatException( + 'A git description cannot have both a ref and a `tag_pattern`.', + ); + } if (languageVersion.forbidsUnknownDescriptionKeys) { for (final key in description.keys) { - if (!['url', 'ref', 'path'].contains(key)) { + if (!['url', 'ref', 'path', 'tag_pattern'].contains(key)) { throw FormatException('Unknown key "$key" in description.'); } } } } - final containingDir = switch (containingDescription) { + final containingDir = switch (containingDescription?.description) { RootDescription(path: final path) => path, PathDescription(path: final path) => path, _ => null, @@ -99,6 +128,7 @@ class GitSource extends CachedSource { containingDir: containingDir, ref: ref, path: _validatedPath(path), + tagPattern: tagPattern, ), ); } @@ -140,6 +170,15 @@ class GitSource extends CachedSource { 'must be a string.', ); } + + final tagPattern = description['tag_pattern']; + if (tagPattern is! String?) { + throw const FormatException( + "The 'tag_pattern' field of the description " + 'must be a string.', + ); + } + return PackageId( name, version, @@ -149,6 +188,7 @@ class GitSource extends CachedSource { ref: ref, path: _validatedPath(description['path']), containingDir: containingDir, + tagPattern: tagPattern, ), resolvedRef, ), @@ -251,23 +291,35 @@ class GitSource extends CachedSource { String? path, SystemCache cache, { required String relativeTo, + required String? tagPattern, }) async { + if (ref != null && tagPattern != null) { + fail('Cannot have both a `tagPattern` and a `ref`'); + } final description = GitDescription( url: url, ref: ref, path: path, containingDir: relativeTo, + tagPattern: tagPattern, // TODO ); return await _pool.withResource(() async { await _ensureRepoCache(description, cache); final path = _repoCachePath(description, cache); - final revision = await _firstRevision(path, description.ref); + + final revision = + tagPattern != null + ? (await _listTaggedVersions( + path, + compileTagPattern(tagPattern), + )).last.commitId + : await _firstRevision(path, description.ref); final resolvedDescription = ResolvedGitDescription(description, revision); return Pubspec.parse( await _showFileAtRevision(resolvedDescription, 'pubspec.yaml', cache), cache.sources, - containingDescription: description, + containingDescription: resolvedDescription, ).name; }); } @@ -322,32 +374,68 @@ class GitSource extends CachedSource { return await _pool.withResource(() async { await _ensureRepoCache(description, cache); final path = _repoCachePath(description, cache); - final revision = await _firstRevision(path, description.ref); - final pubspec = await _describeUncached(ref, revision, cache); - - return [ - PackageId( - ref.name, - pubspec.version, - ResolvedGitDescription(description, revision), - ), - ]; + final result = []; + if (description.tagPattern case final String tagPattern) { + final versions = await _listTaggedVersions( + path, + compileTagPattern(tagPattern), + ); + for (final version in versions) { + result.add( + PackageId( + ref.name, + version.version, + ResolvedGitDescription(description, version.commitId), + ), + ); + } + return result; + } else { + final revision = await _firstRevision(path, description.ref); + + final Pubspec pubspec; + pubspec = await _describeUncached(ref, revision, cache); + result.add( + PackageId( + ref.name, + pubspec.version, + ResolvedGitDescription(description, revision), + ), + ); + return [ + PackageId( + ref.name, + pubspec.version, + ResolvedGitDescription(description, revision), + ), + ]; + } }); } /// Since we don't have an easy way to read from a remote Git repo, this /// just installs [id] into the system cache, then describes it from there. @override - Future describeUncached(PackageId id, SystemCache cache) { + Future describeUncached(PackageId id, SystemCache cache) async { final description = id.description; if (description is! ResolvedGitDescription) { throw StateError('Called with wrong ref'); } - return _pool.withResource( + final pubspec = await _pool.withResource( () => _describeUncached(id.toRef(), description.resolvedRef, cache), ); + if (pubspec.version != id.version) { + throw PackageNotFoundException( + 'Expected ${id.name} version ${id.version} ' + 'at commit ${description.resolvedRef}, ' + 'found ${pubspec.version}.', + ); + } + return pubspec; } + final Map<(PackageRef, String), Pubspec> _pubspecAtRevisionCache = {}; + /// Like [describeUncached], but takes a separate [ref] and Git [revision] /// rather than a single ID. Future _describeUncached( @@ -359,18 +447,16 @@ class GitSource extends CachedSource { if (description is! GitDescription) { throw ArgumentError('Wrong source'); } - await _ensureRevision(description, revision, cache); - - return Pubspec.parse( - await _showFileAtRevision( - ResolvedGitDescription(description, revision), - 'pubspec.yaml', - cache, - ), - cache.sources, - expectedName: ref.name, - containingDescription: ref.description, - ); + return _pubspecAtRevisionCache[(ref, revision)] ??= await () async { + await _ensureRevision(description, revision, cache); + final resolvedDescription = ResolvedGitDescription(description, revision); + return Pubspec.parse( + await _showFileAtRevision(resolvedDescription, 'pubspec.yaml', cache), + cache.sources, + expectedName: ref.name, + containingDescription: resolvedDescription, + ); + }(); } /// Clones a Git repo to the local filesystem. @@ -680,6 +766,34 @@ class GitSource extends CachedSource { String _packageListPath(String revisionCachePath) => p.join(revisionCachePath, '.git/pub-packages'); + /// + Future> _listTaggedVersions( + String path, + RegExp compiledTagPattern, + ) async { + final output = await git.run([ + 'tag', + '--list', + '--format', + // We can use space here, as it is not allowed in a git tag + // https://git-scm.com/docs/git-check-ref-format + '%(refname:lstrip=2) %(objectname)', + ], workingDir: path); + final lines = output.trim().split('\n'); + final result = []; + for (final line in lines) { + final parts = line.split(' '); + if (parts.length != 2) { + throw PackageNotFoundException('Bad output from `git tag --list`'); + } + final match = compiledTagPattern.firstMatch(parts[0]); + if (match == null) continue; + final version = Version.parse(match[1]!); + result.add((version: version, commitId: parts[1])); + } + return result; + } + /// Runs "git rev-list" on [reference] in [path] and returns the first result. /// /// This assumes that the canonical clone already exists. @@ -793,6 +907,8 @@ class GitDescription extends Description { /// not allow strings of the form: 'git@github.com:dart-lang/pub.git'. final String url; + final String? tagPattern; + /// `true` if [url] was parsed from a relative url. final bool relative; @@ -804,11 +920,14 @@ class GitDescription extends Description { /// Represented as a relative url. final String path; + late final RegExp compiledTagPattern = compileTagPattern(tagPattern!); + GitDescription.raw({ required this.url, required this.relative, required String? ref, required String? path, + required this.tagPattern, }) : ref = ref ?? 'HEAD', path = path ?? '.'; @@ -817,6 +936,7 @@ class GitDescription extends Description { required String? ref, required String? path, required String? containingDir, + required String? tagPattern, }) { final validatedUrl = GitSource._validatedUrl(url, containingDir); return GitDescription.raw( @@ -824,6 +944,7 @@ class GitDescription extends Description { relative: validatedUrl.wasRelative, ref: ref, path: path, + tagPattern: tagPattern, ); } @@ -853,6 +974,7 @@ class GitDescription extends Description { 'url': relativeUrl, if (ref != 'HEAD') 'ref': ref, if (path != '.') 'path': path, + if (tagPattern != null) 'tag_pattern': tagPattern, }; } @@ -867,8 +989,13 @@ class GitDescription extends Description { other.path == path; } - GitDescription withRef(String newRef) => - GitDescription.raw(url: url, relative: relative, ref: newRef, path: path); + GitDescription withRef(String newRef) => GitDescription.raw( + url: url, + relative: relative, + ref: newRef, + path: path, + tagPattern: tagPattern, + ); @override int get hashCode => Object.hash(url, ref, path); @@ -884,6 +1011,9 @@ class GitDescription extends Description { } return p.prettyUri(url); } + + @override + bool get hasMultipleVersions => tagPattern != null; } class ResolvedGitDescription extends ResolvedDescription { @@ -915,6 +1045,7 @@ class ResolvedGitDescription extends ResolvedDescription { return { 'url': url, 'ref': description.ref, + if (description.tagPattern != null) 'tag-pattern': description.tagPattern, 'resolved-ref': resolvedRef, 'path': description.path, }; @@ -943,3 +1074,33 @@ String _gitDirArg(String path) { Platform.isWindows ? path.replaceAll('\\', '/') : path; return '--git-dir=$forwardSlashPath'; } + +final tagPatternPattern = RegExp(r'^(.*){{version}}(.*)$'); + +// Adapted from pub_semver-2.1.4/lib/src/version.dart +const versionPattern = + r'(\d+)\.(\d+)\.(\d+)' // Version number. + r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. + r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'; // build + +/// Takes a [tagPattern] and returns a [RegExp] matching the relevant tags. +/// +/// The tagPattern should contain '{{version}}' which will match a pub_semver +/// version. The rest of the tagPattern is matched verbatim. +RegExp compileTagPattern(String tagPattern) { + final match = tagPatternPattern.firstMatch(tagPattern); + if (match == null) { + throw const FormatException( + 'The `tag_pattern` must contain "{{version}" ' + 'to match different versions', + ); + } + final before = RegExp.escape(match[1]!); + final after = RegExp.escape(match[2]!); + + return RegExp( + r'^' + '$before($versionPattern)$after' + r'$', + ); +} diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 1956d9a66..54b98381b 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart @@ -123,8 +123,6 @@ class HostedSource extends CachedSource { @override final name = 'hosted'; - @override - final hasMultipleVersions = true; static String pubDevUrl = 'https://pub.dev'; static String pubDartlangUrl = 'https://pub.dartlang.org'; @@ -229,7 +227,7 @@ class HostedSource extends CachedSource { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, required LanguageVersion languageVersion, }) { return PackageRef( @@ -416,17 +414,22 @@ class HostedSource extends CachedSource { if (pubspecData is! Map) { throw const FormatException('pubspec must be a map'); } + + final archiveSha256 = map['archive_sha256']; + if (archiveSha256 != null && archiveSha256 is! String) { + throw const FormatException('archive_sha256 must be a String'); + } + final parsedContentHash = _parseContentHash(archiveSha256 as String?); final pubspec = Pubspec.fromMap( pubspecData, cache.sources, expectedName: ref.name, location: location, - containingDescription: description, + containingDescription: ResolvedHostedDescription( + description, + sha256: parsedContentHash, + ), ); - final archiveSha256 = map['archive_sha256']; - if (archiveSha256 != null && archiveSha256 is! String) { - throw const FormatException('archive_sha256 must be a String'); - } final archiveUrl = map['archive_url']; if (archiveUrl is! String) { throw const FormatException('archive_url must be a String'); @@ -451,7 +454,6 @@ class HostedSource extends CachedSource { } advisoriesDate = DateTime.parse(advisoriesUpdated); } - final status = PackageStatus( isDiscontinued: isDiscontinued, discontinuedReplacedBy: replacedBy, @@ -463,7 +465,7 @@ class HostedSource extends CachedSource { pubspec, Uri.parse(archiveUrl), status, - _parseContentHash(archiveSha256 as String?), + parsedContentHash, ); }).toList(); } @@ -543,16 +545,20 @@ class HostedSource extends CachedSource { if (listing == null || listing.isEmpty) return; final latestVersion = maxBy<_VersionInfo, Version>(listing, (e) => e.version)!; - final dependencies = latestVersion.pubspec.dependencies.values; - unawaited( - withDependencyType(DependencyType.none, () async { - for (final packageRange in dependencies) { - if (packageRange.source is HostedSource) { - preschedule!(_RefAndCache(packageRange.toRef(), cache)); + try { + final dependencies = latestVersion.pubspec.dependencies.values; + unawaited( + withDependencyType(DependencyType.none, () async { + for (final packageRange in dependencies) { + if (packageRange.source is HostedSource) { + preschedule!(_RefAndCache(packageRange.toRef(), cache)); + } } - } - }), - ); + }), + ); + } on FormatException { + // Ignore malformed dependencies. + } } final cache = refAndCache.cache; @@ -1640,7 +1646,7 @@ See $contentHashesDocumentationUrl. containingDescription: // Dummy description. As we never use the dependencies, they don't // need to be resolved. - RootDescription('.'), + ResolvedRootDescription.fromDir('.'), ); final errors = pubspec.dependencyErrors; if (errors.isNotEmpty) { @@ -1813,6 +1819,9 @@ class HostedDescription extends Description { @override HostedSource get source => HostedSource.instance; + + @override + bool get hasMultipleVersions => true; } class ResolvedHostedDescription extends ResolvedDescription { diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart index 46b6981bd..a5939e771 100644 --- a/lib/src/source/path.dart +++ b/lib/src/source/path.dart @@ -54,7 +54,7 @@ class PathSource extends Source { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, LanguageVersion? languageVersion, }) { if (description is! String) { @@ -64,30 +64,32 @@ class PathSource extends Source { // Resolve the path relative to the containing file path, and remember // whether the original path was relative or absolute. final isRelative = p.isRelative(dir); - - if (containingDescription is PathDescription) { + if (containingDescription is ResolvedPathDescription) { return PackageRef( name, PathDescription( isRelative - ? p.join(p.absolute(containingDescription.path), dir) + ? p.join(p.absolute(containingDescription.description.path), dir) : dir, isRelative, ), ); - } else if (containingDescription is RootDescription) { + } else if (containingDescription is ResolvedRootDescription) { return PackageRef( name, PathDescription( isRelative ? p.normalize( - p.join(p.absolute(containingDescription.path), description), + p.join( + p.absolute(containingDescription.description.path), + description, + ), ) : description, isRelative, ), ); - } else if (containingDescription is GitDescription) { + } else if (containingDescription is ResolvedGitDescription) { if (!isRelative) { throw FormatException( '"$description" is an absolute path, ' @@ -95,7 +97,10 @@ class PathSource extends Source { ); } final resolvedPath = p.url.normalize( - p.url.joinAll([containingDescription.path, ...p.posix.split(dir)]), + p.url.joinAll([ + containingDescription.description.path, + ...p.posix.split(dir), + ]), ); if (!(p.isWithin('.', resolvedPath) || p.equals('.', resolvedPath))) { throw FormatException( @@ -106,9 +111,11 @@ class PathSource extends Source { return PackageRef( name, GitDescription.raw( - url: containingDescription.url, - relative: containingDescription.relative, - ref: containingDescription.ref, + url: containingDescription.description.url, + relative: containingDescription.description.relative, + // Always refer to the same commit as the containing pubspec. + ref: containingDescription.resolvedRef, + tagPattern: null, path: resolvedPath, ), ); @@ -193,12 +200,9 @@ class PathSource extends Source { } // There's only one package ID for a given path. We just need to find the // version. - final pubspec = _loadPubspec(ref, cache); - final id = PackageId( - ref.name, - pubspec.version, - ResolvedPathDescription(description), - ); + final resolvedDescription = ResolvedPathDescription(description); + final pubspec = _loadPubspec(ref, resolvedDescription, cache); + final id = PackageId(ref.name, pubspec.version, resolvedDescription); // Store the pubspec in memory if we need to refer to it again. cache.cachedPubspecs[id] = pubspec; return [id]; @@ -206,14 +210,18 @@ class PathSource extends Source { @override Future doDescribe(PackageId id, SystemCache cache) async => - _loadPubspec(id.toRef(), cache); + _loadPubspec( + id.toRef(), + id.description as ResolvedPathDescription, + cache, + ); - Pubspec _loadPubspec(PackageRef ref, SystemCache cache) { - final description = ref.description; - if (description is! PathDescription) { - throw ArgumentError('Wrong source'); - } - final dir = _validatePath(ref.name, description); + Pubspec _loadPubspec( + PackageRef ref, + ResolvedPathDescription description, + SystemCache cache, + ) { + final dir = _validatePath(ref.name, description.description); return Pubspec.load( dir, cache.sources, @@ -306,6 +314,9 @@ class PathDescription extends Description { @override int get hashCode => _canonicalizedPath.hashCode; + + @override + bool get hasMultipleVersions => false; } class ResolvedPathDescription extends ResolvedDescription { diff --git a/lib/src/source/root.dart b/lib/src/source/root.dart index d6f971ac5..44342bc63 100644 --- a/lib/src/source/root.dart +++ b/lib/src/source/root.dart @@ -66,7 +66,7 @@ class RootSource extends Source { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, required LanguageVersion languageVersion, }) { throw UnsupportedError('Trying to parse a root package description.'); @@ -78,6 +78,7 @@ class ResolvedRootDescription extends ResolvedDescription { RootDescription get description => super.description as RootDescription; ResolvedRootDescription(RootDescription super.description); + ResolvedRootDescription.fromDir(String dir) : super(RootDescription(dir)); @override Object? serializeForLockfile({required String? containingDir}) { @@ -118,4 +119,7 @@ class RootDescription extends Description { @override int get hashCode => 'root'.hashCode; + + @override + bool get hasMultipleVersions => false; } diff --git a/lib/src/source/sdk.dart b/lib/src/source/sdk.dart index daaca0920..8469ef6b9 100644 --- a/lib/src/source/sdk.dart +++ b/lib/src/source/sdk.dart @@ -29,7 +29,7 @@ class SdkSource extends Source { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, LanguageVersion? languageVersion, }) { if (description is! String) { @@ -91,7 +91,9 @@ class SdkSource extends Source { _verifiedPackagePath(ref), cache.sources, expectedName: ref.name, - containingDescription: ref.description, + containingDescription: ResolvedSdkDescription( + ref.description as SdkDescription, + ), ); /// Validate that there are no non-sdk dependencies if the SDK does not @@ -186,6 +188,9 @@ class SdkDescription extends Description { bool operator ==(Object other) { return other is SdkDescription && other.sdk == sdk; } + + @override + bool get hasMultipleVersions => false; } class ResolvedSdkDescription extends ResolvedDescription { diff --git a/lib/src/source/unknown.dart b/lib/src/source/unknown.dart index 50102eaac..8774898c4 100644 --- a/lib/src/source/unknown.dart +++ b/lib/src/source/unknown.dart @@ -37,7 +37,7 @@ class UnknownSource extends Source { PackageRef parseRef( String name, Object? description, { - required Description containingDescription, + required ResolvedDescription containingDescription, LanguageVersion? languageVersion, }) => PackageRef(name, UnknownDescription(description, this)); @@ -111,6 +111,9 @@ class UnknownDescription extends Description { @override int get hashCode => Object.hash(source.name, json.encode(description)); + + @override + bool get hasMultipleVersions => false; } class ResolvedUnknownDescription extends ResolvedDescription { diff --git a/test/descriptor/git.dart b/test/descriptor/git.dart index c52b7dabb..15a43ac7a 100644 --- a/test/descriptor/git.dart +++ b/test/descriptor/git.dart @@ -36,6 +36,15 @@ class GitRepoDescriptor extends DirectoryDescriptor { ]); } + /// Adds a tag named [tag] to the repo described by `this`. + /// + /// [parent] defaults to [sandbox]. + Future tag(String tag, [String? parent]) async { + await _runGitCommands(parent, [ + ['tag', '-a', tag, '-m', 'Some message'], + ]); + } + /// Return a Future that completes to the commit in the git repository /// referred to by [ref]. /// diff --git a/test/get/git/check_out_and_upgrade_test.dart b/test/get/git/check_out_and_upgrade_test.dart index 2bf074327..bbe598a65 100644 --- a/test/get/git/check_out_and_upgrade_test.dart +++ b/test/get/git/check_out_and_upgrade_test.dart @@ -61,4 +61,77 @@ void main() { expect(packageSpec('foo'), isNot(originalFooSpec)); }); + + test('checks out and upgrades a package from with a tag-pattern', () async { + ensureGit(); + + final repo = d.git('foo.git', [ + d.libDir('foo'), + d.libPubspec('foo', '1.0.0'), + ]); + await repo.create(); + await repo.tag('v1.0.0'); + + await d + .appDir( + dependencies: { + 'foo': { + 'git': {'url': '../foo.git', 'tag_pattern': 'v{{version}}'}, + 'version': '^1.0.0', + }, + }, + pubspec: { + 'environment': {'sdk': '^3.9.0'}, + }, + ) + .create(); + + await pubGet( + output: contains('+ foo 1.0.0'), + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + ); + + // This should be found by `pub upgrade`. + await d.git('foo.git', [ + d.libDir('foo'), + d.libPubspec('foo', '1.5.0'), + ]).commit(); + await repo.tag('v1.5.0'); + + // The untagged version should not be found by `pub upgrade`. + await d.git('foo.git', [ + d.libDir('foo'), + d.libPubspec('foo', '1.7.0'), + ]).commit(); + + // This should be found by `pub upgrade --major-versions` + await d.git('foo.git', [ + d.libDir('foo'), + d.libPubspec('foo', '2.0.0'), + ]).commit(); + await repo.tag('v2.0.0'); + + // A version that is not tagged according to the pattern should not be + // chosen by the `upgrade --major-versions`. + await d.git('foo.git', [ + d.libDir('foo'), + d.libPubspec('foo', '3.0.0'), + ]).commit(); + await repo.tag('unrelatedTag'); + + await pubUpgrade( + output: allOf(contains('> foo 1.5.0'), contains('Changed 1 dependency!')), + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + ); + + await pubUpgrade( + args: ['--major-versions'], + output: allOf( + contains('> foo 2.0.0'), + contains('foo: ^1.0.0 -> ^2.0.0'), + contains('Changed 1 dependency!'), + ), + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + ); + }); } diff --git a/test/get/git/ssh_url_test.dart b/test/get/git/ssh_url_test.dart index 81bf45e69..43c13f612 100644 --- a/test/get/git/ssh_url_test.dart +++ b/test/get/git/ssh_url_test.dart @@ -23,6 +23,11 @@ void main() { ref: 'main', path: 'abc/', containingDir: null, + tagPattern: null, + ); + expect( + description.format(), + 'git@github.com:dart-lang/pub.git at main in abc/', ); expect( description.format(), diff --git a/test/get/git/tag_pattern_test.dart b/test/get/git/tag_pattern_test.dart new file mode 100644 index 000000000..b7bf8d6eb --- /dev/null +++ b/test/get/git/tag_pattern_test.dart @@ -0,0 +1,214 @@ +// Copyright (c) 2024, 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:path/path.dart' as p; +import 'package:pub/src/exit_codes.dart'; +import 'package:test/test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +void main() { + test('Versions inside a tag_pattern dependency can depend on versions from ' + 'another commit', () async { + ensureGit(); + await d.git('foo.git', [ + d.libPubspec( + 'foo', + '1.0.0', + sdk: '^3.9.0', + deps: { + 'bar': { + 'git': { + 'url': p.join(d.sandbox, 'bar'), + 'tag_pattern': '{{version}}', + }, + 'version': '^2.0.0', + }, + }, + ), + ]).create(); + await d.git('foo.git', []).tag('1.0.0'); + + await d.git('foo.git', [ + d.libPubspec( + 'foo', + '2.0.0', + sdk: '^3.9.0', + deps: { + 'bar': { + 'git': { + 'url': p.join(d.sandbox, 'bar.git'), + 'tag_pattern': '{{version}}', + }, + 'version': '^1.0.0', + }, + }, + ), + ]).commit(); + await d.git('foo.git', []).tag('2.0.0'); + + await d.git('bar.git', [ + d.libPubspec( + 'bar', + '1.0.0', + sdk: '^3.9.0', + deps: { + 'foo': { + 'git': { + 'url': p.join(d.sandbox, 'bar.git'), + 'tag_pattern': '{{version}}', + }, + 'version': '^2.0.0', + }, + }, + ), + ]).create(); + await d.git('bar.git', []).tag('1.0.0'); + + await d.git('bar.git', [ + d.libPubspec( + 'bar', + '2.0.0', + sdk: '^3.9.0', + deps: { + 'foo': { + 'git': { + 'url': p.join(d.sandbox, 'foo.git'), + 'tag_pattern': '{{version}}', + }, + 'version': '^1.0.0', + }, + }, + ), + ]).commit(); + await d.git('bar.git', []).tag('2.0.0'); + + await d + .appDir( + dependencies: { + 'foo': { + 'git': { + 'url': p.join(d.sandbox, 'foo.git'), + 'tag_pattern': '{{version}}', + }, + 'version': '^1.0.0', + }, + }, + pubspec: { + 'environment': {'sdk': '^3.9.0'}, + }, + ) + .create(); + + await pubGet( + output: allOf(contains('+ foo 1.0.0'), contains('+ bar 2.0.0')), + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + ); + }); + + test('Versions inside a tag_pattern dependency cannot depend on ' + 'version from another commit via path-dependencies', () async { + ensureGit(); + + await d.git('repo.git', [ + d.dir('foo', [ + d.libPubspec( + 'foo', + '1.0.0', + deps: { + 'bar': {'path': '../bar', 'version': '^2.0.0'}, + }, + ), + ]), + d.dir('bar', [ + d.libPubspec( + 'bar', + '2.0.0', + deps: { + 'foo': {'path': '../foo', 'version': '^1.0.0'}, + }, + ), + ]), + ]).create(); + await d.git('repo.git', []).tag('foo-1.0.0'); + await d.git('repo.git', []).tag('bar-2.0.0'); + + await d.git('repo.git', [ + d.dir('foo', [ + d.libPubspec( + 'foo', + '2.0.0', + deps: { + 'bar': {'path': '../bar', 'version': '^2.0.0'}, + }, + ), + ]), + d.dir('bar', [ + d.libPubspec( + 'bar', + '1.0.0', + deps: { + 'foo': {'path': '../foo', 'version': '^1.0.0'}, + }, + ), + ]), + ]).commit(); + await d.git('repo.git', []).tag('foo-2.0.0'); + await d.git('repo.git', []).tag('bar-1.0.0'); + + await d + .appDir( + dependencies: { + 'foo': { + 'git': { + 'url': '../repo.git', + 'tag_pattern': 'foo-{{version}}', + 'path': 'foo', + }, + 'version': '^1.0.0', + }, + }, + pubspec: { + 'environment': {'sdk': '^3.9.0'}, + }, + ) + .create(); + final s = RegExp.escape(p.separator); + await pubGet( + error: matches( + 'Because foo from git ..${s}repo.git at HEAD in foo ' + 'depends on bar \\^2.0.0 from git ' + 'which depends on foo from git ..${s}repo.git at [a-f0-9]+ in foo, ' + 'foo <2.0.0 from git is forbidden', + ), + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + ); + }); + + test('tag_pattern must contain "{{version}}"', () async { + await d + .appDir( + dependencies: { + 'foo': { + 'git': {'url': 'some/git/path', 'tag_pattern': 'v100'}, + }, + }, + pubspec: { + 'environment': {'sdk': '^3.9.0'}, + }, + ) + .create(); + + await pubGet( + environment: {'_PUB_TEST_SDK_VERSION': '3.9.0'}, + error: contains( + 'Invalid description in the "myapp" pubspec on the "foo" dependency: ' + 'The `tag_pattern` must contain "{{version}" ' + 'to match different versions', + ), + exitCode: DATA, + ); + }); +} diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index 60db3101d..a3e2ba3f6 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart @@ -26,7 +26,7 @@ void main() { void Function(Pubspec) fn, { String? expectedContains, String? hintContains, - Description? containingDescription, + ResolvedDescription? containingDescription, }) { var expectation = const TypeMatcher(); if (expectedContains != null) { @@ -47,7 +47,8 @@ void main() { final pubspec = Pubspec.parse( contents, sources, - containingDescription: containingDescription ?? RootDescription('.'), + containingDescription: + containingDescription ?? ResolvedRootDescription.fromDir('.'), ); expect(() => fn(pubspec), throwsA(expectation)); } @@ -57,7 +58,7 @@ void main() { Pubspec.parse( 'version: not a semver', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); }); @@ -68,7 +69,7 @@ void main() { 'name: foo', sources, expectedName: 'bar', - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ), throwsPubspecException, ); @@ -81,7 +82,7 @@ void main() { '{}', sources, expectedName: 'bar', - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ), throwsPubspecException, ); @@ -98,7 +99,7 @@ dependencies: version: ">=1.2.3 <3.4.5" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -119,7 +120,7 @@ dependencies: version: ">=1.2.3 <0.0.0" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -133,7 +134,7 @@ dependencies: dependencies: ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.dependencies, isEmpty); @@ -150,7 +151,7 @@ dev_dependencies: version: ">=1.2.3 <3.4.5" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.devDependencies['foo']!; @@ -166,7 +167,7 @@ dev_dependencies: dev_dependencies: ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.devDependencies, isEmpty); @@ -183,7 +184,7 @@ dependency_overrides: version: ">=1.2.3 <3.4.5" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencyOverrides['foo']!; @@ -199,7 +200,7 @@ dependency_overrides: dependency_overrides: ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.dependencyOverrides, isEmpty); @@ -213,7 +214,7 @@ dependencies: unknown: blah ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -229,7 +230,7 @@ dependencies: version: 1.2.3 ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -344,7 +345,7 @@ environment: workspace: ['a', 'b', 'c'] ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ).workspace, ['a', 'b', 'c'], ); @@ -359,7 +360,7 @@ environment: resolution: workspace ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ).resolution, Resolution.workspace, ); @@ -393,7 +394,7 @@ environment: resolution: workspace ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ).name, 'foo', ); @@ -451,7 +452,7 @@ resolution: "sometimes"''', (pubspec) => pubspec.resolution); # See https://dart.dev/tools/pub/cmd for details ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.version, equals(Version.none)); expect(pubspec.dependencies, isEmpty); @@ -464,13 +465,15 @@ name: pkg dependencies: from_path: {path: non_local_path} ''', - containingDescription: HostedDescription('foo', 'https://pub.dev'), + containingDescription: ResolvedHostedDescription( + HostedDescription('foo', 'https://pub.dev'), + sha256: null, + ), (pubspec) => pubspec.dependencies, expectedContains: 'Invalid description in the "pkg" pubspec on the "from_path" ' - 'dependency: "non_local_path" is a relative path, ' - 'but this isn\'t a ' - 'local pubspec.', + 'dependency: "non_local_path" is a path, but ' + 'this isn\'t a local pubspec.', ); }); @@ -486,7 +489,7 @@ dependencies: name: bar ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -513,7 +516,7 @@ dependencies: url: https://example.org/pub/ ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -539,7 +542,7 @@ dependencies: hosted: https://example.org/pub/ ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -565,7 +568,7 @@ dependencies: hosted: bar ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -593,7 +596,7 @@ dependencies: hosted: https://example.org/pub/ ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( @@ -617,7 +620,7 @@ dependencies: foo: ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final foo = pubspec.dependencies['foo']!; @@ -715,7 +718,7 @@ dependencies: final pubspec = Pubspec.parse( 'name: testing', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( pubspec.dartSdkConstraint.effectiveConstraint, @@ -730,7 +733,7 @@ dependencies: final pubspec = Pubspec.parse( '', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( pubspec.dartSdkConstraint.effectiveConstraint, @@ -748,7 +751,7 @@ dependencies: sdk: ">1.0.0" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( pubspec.dartSdkConstraint.effectiveConstraint, @@ -766,7 +769,7 @@ dependencies: sdk: ">3.0.0" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( pubspec.sdkConstraints, @@ -795,7 +798,7 @@ environment: fuchsia: ^5.6.7 ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect( pubspec.sdkConstraints, @@ -859,7 +862,7 @@ environment: final pubspec = Pubspec.parse( '', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.publishTo, isNull); }); @@ -877,7 +880,7 @@ environment: publish_to: http://example.com ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.publishTo, equals('http://example.com')); }); @@ -888,7 +891,7 @@ publish_to: http://example.com publish_to: none ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.publishTo, equals('none')); }); @@ -913,7 +916,7 @@ publish_to: none final pubspec = Pubspec.parse( '', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.executables, isEmpty); }); @@ -925,7 +928,7 @@ executables: abcDEF-123_: "abc DEF-123._" ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.executables['abcDEF-123_'], equals('abc DEF-123._')); }); @@ -979,7 +982,7 @@ executables: command: ''', sources, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); expect(pubspec.executables['command'], equals('command')); }); @@ -998,7 +1001,7 @@ dependency_overrides: sources, overridesFileContents: overridesContents, overridesLocation: Uri.parse('file:///pubspec_overrides.yaml'), - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); } diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index c0ec3709f..dcf5a4241 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart @@ -520,26 +520,34 @@ Because myapp depends on foo ^1.0.0 which doesn't match any versions, version so test('mismatched sources', () async { await d.dir('shared', [d.libPubspec('shared', '1.0.0')]).create(); - - await servePackages() - ..serve('foo', '1.0.0', deps: {'shared': '1.0.0'}) - ..serve( + await d.dir('bar', [ + d.libPubspec( 'bar', '1.0.0', deps: { 'shared': {'path': p.join(d.sandbox, 'shared')}, }, - ) + ), + ]).create(); + await servePackages() + ..serve('foo', '1.0.0', deps: {'shared': '1.0.0'}) ..serve('shared', '1.0.0'); - await d.appDir(dependencies: {'foo': '1.0.0', 'bar': '1.0.0'}).create(); + await d + .appDir( + dependencies: { + 'foo': '1.0.0', + 'bar': {'path': '../bar'}, + }, + ) + .create(); await expectResolves( error: equalsIgnoringWhitespace(''' - Because every version of bar depends on shared from path and every - version of foo depends on shared from hosted, bar is incompatible with - foo. - So, because myapp depends on both foo 1.0.0 and bar 1.0.0, version - solving failed. + Because every version of bar from path depends on shared + from path and every version of foo depends on shared from hosted, + bar from path is incompatible with foo. +So, because myapp depends on both foo 1.0.0 and bar from path, +version solving failed. '''), ); }); @@ -888,7 +896,7 @@ void backtracking() { // dependencies are traversed breadth-first (all of myapps's immediate deps // before any other their deps). // - // This means it doesn't discover the source conflict until after selecting + // This means it doesn't discover the version conflict until after selecting // c. When that happens, it should backjump past c instead of trying older // versions of it since they aren't related to the conflict. test('successful backjump to conflicting source', () async { @@ -896,21 +904,18 @@ void backtracking() { await servePackages() ..serve('a', '1.0.0') + ..serve('a', '2.0.0') ..serve('b', '1.0.0', deps: {'a': 'any'}) - ..serve( - 'b', - '2.0.0', - deps: { - 'a': {'path': p.join(d.sandbox, 'a')}, - }, - ) + ..serve('b', '2.0.0', deps: {'a': '^2.0.0'}) ..serve('c', '1.0.0') ..serve('c', '2.0.0') ..serve('c', '3.0.0') ..serve('c', '4.0.0') ..serve('c', '5.0.0'); - await d.appDir(dependencies: {'a': 'any', 'b': 'any', 'c': 'any'}).create(); + await d + .appDir(dependencies: {'a': '1.0.0', 'b': 'any', 'c': 'any'}) + .create(); await expectResolves(result: {'a': '1.0.0', 'b': '1.0.0', 'c': '5.0.0'}); }); @@ -945,29 +950,39 @@ void backtracking() { // fail in this case with no backtracking. test('failing backjump to conflicting source', () async { await d.dir('a', [d.libPubspec('a', '1.0.0')]).create(); - - await servePackages() - ..serve('a', '1.0.0') - ..serve( + await d.dir('b', [ + d.libPubspec( 'b', '1.0.0', deps: { 'a': {'path': p.join(d.sandbox, 'shared')}, }, - ) + ), + ]).create(); + + await servePackages() + ..serve('a', '1.0.0') ..serve('c', '1.0.0') ..serve('c', '2.0.0') ..serve('c', '3.0.0') ..serve('c', '4.0.0') ..serve('c', '5.0.0'); - await d.appDir(dependencies: {'a': 'any', 'b': 'any', 'c': 'any'}).create(); + await d + .appDir( + dependencies: { + 'a': 'any', + 'b': {'path': '../b'}, + 'c': 'any', + }, + ) + .create(); await expectResolves( error: equalsIgnoringWhitespace(''' - Because every version of b depends on a from path and myapp depends on - a from hosted, b is forbidden. - So, because myapp depends on b any, version solving failed. - '''), +Because every version of b from path depends on a from path +and myapp depends on a from hosted, b from path is forbidden. +So, because myapp depends on b from path, version solving failed. +'''), ); }); @@ -1983,7 +1998,7 @@ Future expectResolves({ final resultPubspec = Pubspec.fromMap( {'dependencies': result}, registry, - containingDescription: RootDescription('.'), + containingDescription: ResolvedRootDescription.fromDir('.'), ); final ids = {...lockFile.packages};