Suggest resolution updates (#3569)
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index 7bb02a1..8c38636 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart
@@ -18,6 +18,7 @@ import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; +import '../sdk.dart'; import '../solver.dart'; import '../source/git.dart'; import '../source/hosted.dart'; @@ -612,6 +613,9 @@ { 'dependencies': { packageName: parsedDescriptor, + }, + 'environment': { + 'sdk': sdk.version.toString(), } }, cache.sources,
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart index 0ddf6ef..ced8c00 100644 --- a/lib/src/command/dependency_services.dart +++ b/lib/src/command/dependency_services.dart
@@ -53,7 +53,7 @@ Future<void> runProtected() async { final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec); - final breakingPubspec = stripVersionUpperBounds(compatiblePubspec); + final breakingPubspec = stripVersionBounds(compatiblePubspec); final compatiblePackagesResult = await _tryResolve(compatiblePubspec, cache); @@ -85,7 +85,7 @@ if (package == null) return []; final lockFile = entrypoint.lockFile; final pubspec = upgradeType == _UpgradeType.multiBreaking - ? stripVersionUpperBounds(rootPubspec) + ? stripVersionBounds(rootPubspec) : Pubspec( rootPubspec.name, dependencies: rootPubspec.dependencies.values,
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart index c9e45df..dee80a1 100644 --- a/lib/src/command/outdated.dart +++ b/lib/src/command/outdated.dart
@@ -736,7 +736,7 @@ @override Future<Pubspec> resolvablePubspec(Pubspec? pubspec) async { - return stripVersionUpperBounds(pubspec!); + return stripVersionBounds(pubspec!); } }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 84fe11e..1cfcb59 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -176,7 +176,7 @@ Future<void> _runUpgradeMajorVersions() async { final toUpgrade = _directDependenciesToUpgrade(); - final resolvablePubspec = stripVersionUpperBounds( + final resolvablePubspec = stripVersionBounds( entrypoint.root.pubspec, stripOnly: toUpgrade, );
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 177d6ef..6f8aa75 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -23,8 +23,8 @@ import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; -import 'package_config.dart'; import 'package_config.dart' show PackageConfig; +import 'package_config.dart'; import 'package_graph.dart'; import 'package_name.dart'; import 'pub_embeddable_command.dart'; @@ -32,6 +32,7 @@ import 'sdk.dart'; import 'solver.dart'; import 'solver/report.dart'; +import 'solver/solve_suggestions.dart'; import 'source/cached.dart'; import 'source/unknown.dart'; import 'system_cache.dart'; @@ -355,16 +356,30 @@ } SolveResult result; - result = await log.progress('Resolving dependencies$suffix', () async { - _checkSdkConstraint(root.pubspec); - return resolveVersions( - type, - cache, - root, - lockFile: lockFile, - unlock: unlock ?? [], + + try { + result = await log.progress('Resolving dependencies$suffix', () async { + _checkSdkConstraint(root.pubspec); + return resolveVersions( + type, + cache, + root, + lockFile: lockFile, + unlock: unlock ?? [], + ); + }); + } on SolveFailure catch (e) { + throw SolveFailure( + e.incompatibility, + suggestions: await suggestResolutionAlternatives( + this, + type, + e.incompatibility, + unlock ?? [], + cache, + ), ); - }); + } // We have to download files also with --dry-run to ensure we know the // archive hashes for downloaded files.
diff --git a/lib/src/flutter_releases.dart b/lib/src/flutter_releases.dart new file mode 100644 index 0000000..38e8ca8 --- /dev/null +++ b/lib/src/flutter_releases.dart
@@ -0,0 +1,105 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:http/http.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import 'http.dart'; +import 'log.dart'; + +String get flutterReleasesUrl => + Platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ?? + 'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json'; + +// Retrieves all released versions of Flutter. +Future<List<FlutterRelease>> _flutterReleases = () async { + final response = await retryForHttp( + 'fetching available Flutter releases', + () => globalHttpClient.fetch(Request('GET', Uri.parse(flutterReleasesUrl))), + ); + final decoded = jsonDecode(response.body); + if (decoded is! Map) throw FormatException('Bad response - should be a Map'); + final releases = decoded['releases']; + if (releases is! List) { + throw FormatException('Bad response - releases should be a list.'); + } + final result = <FlutterRelease>[]; + for (final release in releases) { + final channel = { + 'beta': Channel.beta, + 'stable': Channel.stable, + 'dev': Channel.dev + }[release['channel']]; + if (channel == null) throw FormatException('Release with bad channel'); + final dartVersion = release['dart_sdk_version']; + // Some releases don't have an associated dart version, ignore. + if (dartVersion is! String) continue; + final flutterVersion = release['version']; + if (flutterVersion is! String) throw FormatException('Not a string'); + result.add( + FlutterRelease( + flutterVersion: Version.parse(flutterVersion), + dartVersion: Version.parse(dartVersion.split(' ').first), + channel: channel, + ), + ); + } + return result + // Sort releases by channel and version. + .sorted((a, b) { + final compareChannels = b.channel.index - a.channel.index; + if (compareChannels != 0) return compareChannels; + return a.flutterVersion.compareTo(b.flutterVersion); + }) + // Newest first. + .reversed + .toList(); +}(); + +/// The "best" Flutter release for a given set of constraints is the first one +/// in [_flutterReleases] that matches both the flutter and dart constraint. +/// +/// Returns if no such release could be found. +Future<FlutterRelease?> inferBestFlutterRelease( + Map<String, VersionConstraint> sdkConstraints, +) async { + final List<FlutterRelease> flutterReleases; + try { + flutterReleases = await _flutterReleases; + } on Exception catch (e) { + fine('Failed retrieving the list of flutter-releases: $e'); + return null; + } + return flutterReleases.firstWhereOrNull( + (release) => + (sdkConstraints['flutter'] ?? VersionConstraint.any) + .allows(release.flutterVersion) && + (sdkConstraints['dart'] ?? VersionConstraint.any) + .allows(release.dartVersion), + ); +} + +enum Channel { + stable, + beta, + dev, +} + +/// A version of the Flutter SDK and its related Dart SDK. +class FlutterRelease { + final Version flutterVersion; + final Version dartVersion; + final Channel channel; + FlutterRelease({ + required this.flutterVersion, + required this.dartVersion, + required this.channel, + }); + @override + toString() => + 'FlutterRelease(flutter=$flutterVersion, dart=$dartVersion, channel=$channel)'; +}
diff --git a/lib/src/io.dart b/lib/src/io.dart index 8f510f6..086f93f 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart
@@ -1190,6 +1190,6 @@ /// /// Otherwise, wrap with single quotation, and use '\'' to insert single quote. String escapeShellArgument(String x) => - RegExp(r'^[a-zA-Z0-9-_=@.]+$').stringMatch(x) == null + RegExp(r'^[a-zA-Z0-9-_=@.^]+$').stringMatch(x) == null ? "'${x.replaceAll(r'\', r'\\').replaceAll("'", r"'\''")}'" : x;
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart index c46573a..3f57976 100644 --- a/lib/src/pubspec_utils.dart +++ b/lib/src/pubspec_utils.dart
@@ -36,20 +36,22 @@ } /// Returns new pubspec with the same dependencies as [original] but with the -/// upper bounds of the constraints removed. +/// the bounds of the constraints removed. /// -/// If [stripOnly] is provided, only the packages whose names are in -/// [stripOnly] will have their upper bounds removed. If [stripOnly] is -/// not specified or empty, then all packages will have their upper bounds -/// removed. -Pubspec stripVersionUpperBounds( +/// If [stripLower] is `false` (the default) only the upper bound is removed. +/// +/// If [stripOnly] is provided, only the packages whose names are in [stripOnly] +/// will have their bounds removed. If [stripOnly] is not specified or empty, +/// then all packages will have their bounds removed. +Pubspec stripVersionBounds( Pubspec original, { Iterable<String>? stripOnly, + bool stripLowerBound = false, }) { ArgumentError.checkNotNull(original, 'original'); stripOnly ??= []; - List<PackageRange> stripUpperBounds( + List<PackageRange> stripBounds( Map<String, PackageRange> constrained, ) { final result = <PackageRange>[]; @@ -61,7 +63,9 @@ if (stripOnly!.isEmpty || stripOnly.contains(packageRange.name)) { unconstrainedRange = PackageRange( packageRange.toRef(), - stripUpperBound(packageRange.constraint), + stripLowerBound + ? VersionConstraint.any + : stripUpperBound(packageRange.constraint), ); } result.add(unconstrainedRange); @@ -74,8 +78,8 @@ original.name, version: original.version, sdkConstraints: original.sdkConstraints, - dependencies: stripUpperBounds(original.dependencies), - devDependencies: stripUpperBounds(original.devDependencies), + dependencies: stripBounds(original.dependencies), + devDependencies: stripBounds(original.devDependencies), dependencyOverrides: original.dependencyOverrides.values, ); }
diff --git a/lib/src/solver.dart b/lib/src/solver.dart index 74dbfa8..b8f68e7 100644 --- a/lib/src/solver.dart +++ b/lib/src/solver.dart
@@ -4,6 +4,8 @@ import 'dart:async'; +import 'package:pub_semver/pub_semver.dart'; + import 'lock_file.dart'; import 'package.dart'; import 'solver/failure.dart'; @@ -33,6 +35,7 @@ Package root, { LockFile? lockFile, Iterable<String> unlock = const [], + Map<String, Version> sdkOverrides = const {}, }) { lockFile ??= LockFile.empty(); return VersionSolver( @@ -41,6 +44,7 @@ root, lockFile, unlock, + sdkOverrides: sdkOverrides, ).solve(); }
diff --git a/lib/src/solver/failure.dart b/lib/src/solver/failure.dart index ba4e69f..7ccf7a8 100644 --- a/lib/src/solver/failure.dart +++ b/lib/src/solver/failure.dart
@@ -19,6 +19,8 @@ /// it will have one term, which will be the root package. final Incompatibility incompatibility; + final String? suggestions; + @override String get message => toString(); @@ -35,7 +37,7 @@ return null; } - SolveFailure(this.incompatibility) + SolveFailure(this.incompatibility, {this.suggestions}) : assert( incompatibility.terms.isEmpty || incompatibility.terms.single.package.isRoot, @@ -44,7 +46,10 @@ /// Describes how [incompatibility] was derived, and thus why version solving /// failed. @override - String toString() => _Writer(incompatibility).write(); + String toString() => [ + _Writer(incompatibility).write(), + if (suggestions != null) suggestions + ].join('\n'); } /// A class that writes a human-readable description of the cause of a
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart index 3dccd37..053e287 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart
@@ -54,6 +54,8 @@ /// reversed. final bool _isDowngrade; + final Map<String, Version> sdkOverrides; + /// A map from dependency names to constraints indicating which versions of /// [_ref] have already had their dependencies on the given versions returned /// by [incompatibilitiesFor]. @@ -107,11 +109,15 @@ this._overriddenPackages, this._allowedRetractedVersion, { bool downgrade = false, + this.sdkOverrides = const {}, }) : _isDowngrade = downgrade; /// Creates a package lister for the root [package]. - PackageLister.root(Package package, this._systemCache) - : _ref = PackageRef.root(package), + PackageLister.root( + Package package, + this._systemCache, { + required Map<String, Version>? sdkOverrides, + }) : _ref = PackageRef.root(package), // Treat the package as locked so we avoid the logic for finding the // boundaries of various constraints, which is useless for the root // package. @@ -120,7 +126,8 @@ _overriddenPackages = Set.unmodifiable(package.dependencyOverrides.keys), _isDowngrade = false, - _allowedRetractedVersion = null; + _allowedRetractedVersion = null, + sdkOverrides = sdkOverrides ?? {}; /// Returns the number of versions of this package that match [constraint]. Future<int> countVersions(VersionConstraint constraint) async { @@ -461,6 +468,7 @@ if (constraint == null) return true; return sdk.isAvailable && - constraint.effectiveConstraint.allows(sdk.version!); + constraint.effectiveConstraint + .allows(sdkOverrides[sdk.identifier] ?? sdk.version!); } }
diff --git a/lib/src/solver/solve_suggestions.dart b/lib/src/solver/solve_suggestions.dart new file mode 100644 index 0000000..0b381d3 --- /dev/null +++ b/lib/src/solver/solve_suggestions.dart
@@ -0,0 +1,276 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:pub_semver/pub_semver.dart'; + +import '../command_runner.dart'; +import '../entrypoint.dart'; +import '../flutter_releases.dart'; +import '../io.dart'; +import '../package.dart'; +import '../package_name.dart'; +import '../pubspec.dart'; +import '../pubspec_utils.dart'; +import '../solver.dart'; +import '../source/hosted.dart'; +import '../system_cache.dart'; +import 'incompatibility.dart'; +import 'incompatibility_cause.dart'; + +/// Looks through the root-[incompability] of a solve-failure and tries to see if +/// the conflict could resolved by any of the following suggestions: +/// * An update of the current SDK. +/// * Any single change to a package constraint. +/// * Removing the bounds on all constraints, changing less than 5 dependencies. +/// * Running `pub upgrade --major versions`. +/// +/// Returns a formatted list of suggestions, or the empty String if no +/// suggestions were found. +Future<String?> suggestResolutionAlternatives( + Entrypoint entrypoint, + SolveType type, + Incompatibility incompatibility, + Iterable<String> unlock, + SystemCache cache, +) async { + final resolutionContext = _ResolutionContext( + entrypoint: entrypoint, + type: type, + cache: cache, + unlock: unlock, + ); + + final visited = <String>{}; + final stopwatch = Stopwatch()..start(); + final suggestions = <_ResolutionSuggestion>[]; + void addSuggestionIfPresent(_ResolutionSuggestion? suggestion) { + if (suggestion != null) suggestions.add(suggestion); + } + + for (final externalIncompatibility + in incompatibility.externalIncompatibilities) { + if (stopwatch.elapsed > Duration(seconds: 3)) { + // Never spend more than 3 seconds computing suggestions. + break; + } + final cause = externalIncompatibility.cause; + if (cause is SdkCause) { + addSuggestionIfPresent(await resolutionContext.suggestSdkUpdate(cause)); + } else { + for (final term in externalIncompatibility.terms) { + final name = term.package.name; + + if (!visited.add(name)) { + continue; + } + addSuggestionIfPresent( + await resolutionContext.suggestSinglePackageUpdate(name), + ); + } + } + } + if (suggestions.isEmpty) { + addSuggestionIfPresent( + await resolutionContext.suggestUnlockingAll(stripLowerBound: true) ?? + await resolutionContext.suggestUnlockingAll(stripLowerBound: false), + ); + } + + if (suggestions.isEmpty) return null; + final tryOne = suggestions.length == 1 + ? 'You can try the following suggestion to make the pubspec resolve:' + : 'You can try one of the following suggestions to make the pubspec resolve:'; + + suggestions.sort((a, b) => a.priority.compareTo(b.priority)); + + return '\n$tryOne\n${suggestions.take(5).map((e) => e.suggestion).join('\n')}'; +} + +class _ResolutionSuggestion { + final String suggestion; + final int priority; + _ResolutionSuggestion(this.suggestion, {this.priority = 0}); +} + +String packageAddDescription(Entrypoint entrypoint, PackageId id) { + final name = id.name; + final isDev = entrypoint.root.pubspec.devDependencies.containsKey(name); + final resolvedDescription = id.description; + final String descriptor; + final d = resolvedDescription.description.serializeForPubspec( + containingDir: Directory.current + .path // The add command will resolve file names relative to CWD. + // This currently should have no implications as we don't create suggestions + // for path-packages. + , + languageVersion: entrypoint.root.pubspec.languageVersion, + ); + if (d == null) { + descriptor = VersionConstraint.compatibleWith(id.version).toString(); + } else { + descriptor = json.encode({ + 'version': VersionConstraint.compatibleWith(id.version).toString(), + id.source.name: d + }); + } + + final devPart = isDev ? 'dev:' : ''; + return '$devPart$name:${escapeShellArgument(descriptor)}'; +} + +class _ResolutionContext { + final Entrypoint entrypoint; + final SolveType type; + final Iterable<String> unlock; + final SystemCache cache; + _ResolutionContext({ + required this.entrypoint, + required this.type, + required this.cache, + required this.unlock, + }); + + /// If [cause] mentions an sdk, attempt resolving using another released + /// version of Flutter/Dart. Return that as a suggestion if found. + Future<_ResolutionSuggestion?> suggestSdkUpdate(SdkCause cause) async { + final sdkName = cause.sdk.identifier; + if (!(sdkName == 'dart' || (sdkName == 'flutter' && runningFromFlutter))) { + // Only make sdk upgrade suggestions for Flutter and Dart. + return null; + } + + final constraint = cause.constraint; + if (constraint == null) return null; + + /// Find the most relevant Flutter release fullfilling the constraint. + final bestRelease = + await inferBestFlutterRelease({cause.sdk.identifier: constraint}); + if (bestRelease == null) return null; + final result = await _tryResolve( + entrypoint.root.pubspec, + sdkOverrides: { + 'dart': bestRelease.dartVersion, + 'flutter': bestRelease.flutterVersion + }, + ); + if (result == null) { + return null; + } + return _ResolutionSuggestion( + runningFromFlutter + ? '* Try using the Flutter SDK version: ${bestRelease.flutterVersion}. ' + : + // Here we assume that any Dart version included in a Flutter + // release can also be found as a released Dart SDK. + '* Try using the Dart SDK version: ${bestRelease.dartVersion}. See https://dart.dev/get-dart.', + ); + } + + /// Attempt another resolution with a relaxed constraint on [name]. If that + /// resolves, suggest upgrading to that version. + Future<_ResolutionSuggestion?> suggestSinglePackageUpdate(String name) async { + final originalRange = entrypoint.root.dependencies[name] ?? + entrypoint.root.devDependencies[name]; + if (originalRange == null || + originalRange.description is! HostedDescription) { + // We can only relax constraints on hosted dependencies. + return null; + } + final originalConstraint = originalRange.constraint; + final relaxedPubspec = stripVersionBounds( + entrypoint.root.pubspec, + stripOnly: [name], + stripLowerBound: true, + ); + + final result = await _tryResolve(relaxedPubspec); + if (result == null) { + return null; + } + final resolvingPackage = result.packages.firstWhere((p) => p.name == name); + + final addDescription = packageAddDescription(entrypoint, resolvingPackage); + + var priority = 1; + var suggestion = + '* Try updating your constraint on $name: $topLevelProgram pub add $addDescription'; + if (originalConstraint is VersionRange) { + final min = originalConstraint.min; + if (min != null) { + if (resolvingPackage.version < min) { + priority = 3; + suggestion = + '* Consider downgrading your constraint on $name: $topLevelProgram pub add $addDescription'; + } else { + priority = 2; + suggestion = + '* Try upgrading your constraint on $name: $topLevelProgram pub add $addDescription'; + } + } + } + + return _ResolutionSuggestion(suggestion, priority: priority); + } + + /// Attempt resolving with all version constraints relaxed. If that resolves, + /// return a corresponding suggestion to update. + Future<_ResolutionSuggestion?> suggestUnlockingAll({ + required bool stripLowerBound, + }) async { + final originalPubspec = entrypoint.root.pubspec; + final relaxedPubspec = + stripVersionBounds(originalPubspec, stripLowerBound: stripLowerBound); + + final result = await _tryResolve(relaxedPubspec); + if (result == null) { + return null; + } + final updatedPackageVersions = <PackageId>[]; + for (final id in result.packages) { + final originalConstraint = (originalPubspec.dependencies[id.name] ?? + originalPubspec.devDependencies[id.name]) + ?.constraint; + if (originalConstraint != null) { + updatedPackageVersions.add(id); + } + } + if (stripLowerBound && updatedPackageVersions.length > 5) { + // Too complex, don't suggest. + return null; + } + if (stripLowerBound) { + updatedPackageVersions.sort((a, b) => a.name.compareTo(b.name)); + final formattedConstraints = updatedPackageVersions + .map((e) => packageAddDescription(entrypoint, e)) + .join(' '); + return _ResolutionSuggestion( + '* Try updating the following constraints: $topLevelProgram pub add $formattedConstraints', + priority: 4, + ); + } else { + return _ResolutionSuggestion( + '* Try an upgrade of your constraints: $topLevelProgram pub upgrade --major-versions', + priority: 4, + ); + } + } + + /// Attempt resolving + Future<SolveResult?> _tryResolve( + Pubspec pubspec, { + Map<String, Version> sdkOverrides = const {}, + }) async { + try { + return await resolveVersions( + type, + cache, + Package.inMemory(pubspec), + sdkOverrides: sdkOverrides, + lockFile: entrypoint.lockFile, + unlock: unlock, + ); + } on SolveFailure { + return null; + } + } +}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index e8a2346..c08d1ca 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart
@@ -75,6 +75,10 @@ /// The set of packages for which the lockfile should be ignored. final Set<String> _unlock; + /// If present these represents the version of an SDK to assume during + /// resolution. + final Map<String, Version> _sdkOverrides; + final _stopwatch = Stopwatch(); VersionSolver( @@ -82,8 +86,10 @@ this._systemCache, this._root, this._lockFile, - Iterable<String> unlock, - ) : _dependencyOverrides = _root.dependencyOverrides, + Iterable<String> unlock, { + Map<String, Version> sdkOverrides = const {}, + }) : _sdkOverrides = sdkOverrides, + _dependencyOverrides = _root.dependencyOverrides, _unlock = {...unlock}; /// Finds a set of dependencies that match the root package's constraints, or @@ -496,7 +502,13 @@ PackageLister _packageLister(PackageRange package) { var ref = package.toRef(); return _packageListers.putIfAbsent(ref, () { - if (ref.isRoot) return PackageLister.root(_root, _systemCache); + if (ref.isRoot) { + return PackageLister.root( + _root, + _systemCache, + sdkOverrides: _sdkOverrides, + ); + } var locked = _getLocked(ref.name); if (locked != null && locked.toRef() != ref) locked = null; @@ -516,6 +528,7 @@ overridden, _getAllowedRetracted(ref.name), downgrade: _type == SolveType.downgrade, + sdkOverrides: _sdkOverrides, ); }); }
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index ae034d9..e8611d4 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -1399,6 +1399,10 @@ if (url == source.defaultUrl) { return null; } + if (languageVersion >= + LanguageVersion.firstVersionWithShorterHostedSyntax) { + return url; + } return {'url': url, 'name': packageName}; }
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart index f6767a0..7be0095 100644 --- a/test/add/hosted/non_default_pub_server_test.dart +++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -33,11 +33,43 @@ await d.appDir( dependencies: { + 'foo': {'version': '1.2.3', 'hosted': url} + }, + ).validate(); + }); + + test('Uses old syntax when needed', () async { + // Make the default server serve errors. Only the custom server should + // be accessed. + (await servePackages()).serveErrors(); + + final server = await startPackageServer(); + server.serve('foo', '0.2.5'); + server.serve('foo', '1.1.0'); + server.serve('foo', '1.2.3'); + final oldSyntaxSdkConstraint = { + 'environment': { + 'sdk': '>=2.14.0 <3.0.0' // Language version for old syntax. + }, + }; + + await d.appDir( + dependencies: {}, + pubspec: oldSyntaxSdkConstraint, + ).create(); + + final url = server.url; + + await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]); + + await d.appDir( + dependencies: { 'foo': { 'version': '1.2.3', 'hosted': {'name': 'foo', 'url': url} } }, + pubspec: oldSyntaxSdkConstraint, ).validate(); }); @@ -75,18 +107,9 @@ await d.appDir( dependencies: { - 'foo': { - 'version': '1.2.3', - 'hosted': {'name': 'foo', 'url': url} - }, - 'bar': { - 'version': '3.2.3', - 'hosted': {'name': 'bar', 'url': url} - }, - 'baz': { - 'version': '1.3.5', - 'hosted': {'name': 'baz', 'url': url} - } + 'foo': {'version': '1.2.3', 'hosted': url}, + 'bar': {'version': '3.2.3', 'hosted': url}, + 'baz': {'version': '1.3.5', 'hosted': url} }, ).validate(); }); @@ -139,10 +162,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': '^1.2.3', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': '^1.2.3', 'hosted': url} }, ).validate(); }); @@ -170,10 +190,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': '^1.2.3', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': '^1.2.3', 'hosted': url} }, ).validate(); }); @@ -202,10 +219,7 @@ ]).validate(); await d.appDir( dependencies: { - 'foo': { - 'version': 'any', - 'hosted': {'name': 'foo', 'url': url} - } + 'foo': {'version': 'any', 'hosted': url} }, ).validate(); });
diff --git a/test/hosted/offline_test.dart b/test/hosted/offline_test.dart index 5e54c76..da98d0c 100644 --- a/test/hosted/offline_test.dart +++ b/test/hosted/offline_test.dart
@@ -118,10 +118,8 @@ await pubCommand( command, args: ['--offline'], - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo >2.0.0 which doesn't match any - versions, version solving failed. - """), + error: contains(''' +Because myapp depends on foo >2.0.0 which doesn't match any versions, version solving failed.'''), ); });
diff --git a/test/sdk_test.dart b/test/sdk_test.dart index b9a89d5..f409bc7 100644 --- a/test/sdk_test.dart +++ b/test/sdk_test.dart
@@ -127,10 +127,8 @@ await pubCommand( command, environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')}, - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo ^1.0.0 from sdk which doesn't match - any versions, version solving failed. - """), + error: contains(''' +Because myapp depends on foo ^1.0.0 from sdk which doesn't match any versions, version solving failed.'''), ); }); @@ -142,10 +140,8 @@ ).create(); await pubCommand( command, - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo from sdk which doesn't exist - (unknown SDK "unknown"), version solving failed. - """), + error: equalsIgnoringWhitespace(''' +Because myapp depends on foo from sdk which doesn't exist (unknown SDK "unknown"), version solving failed.'''), exitCode: exit_codes.UNAVAILABLE, ); });
diff --git a/test/solve_suggestions_test.dart b/test/solve_suggestions_test.dart new file mode 100644 index 0000000..a419243 --- /dev/null +++ b/test/solve_suggestions_test.dart
@@ -0,0 +1,275 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shelf/shelf.dart'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +void main() { + test('suggests an upgrade to the flutter sdk', () async { + await d.dir('flutter', [d.file('version', '1.2.3')]).create(); + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + pubspec: { + 'environment': {'flutter': '>=3.3.0', 'sdk': '^2.17.0'} + }, + ); + server.handle( + '/flutterReleases', + (request) => Response.ok(releasesMockResponse), + ); + await d.dir(appPath, [ + d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0') + ]).create(); + await pubGet( + error: contains('* Try using the Flutter SDK version: 3.3.2.'), + environment: { + '_PUB_TEST_SDK_VERSION': '2.17.0', + 'FLUTTER_ROOT': path('flutter'), + '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases', + 'PUB_ENVIRONMENT': 'flutter_cli', + }, + ); + }); + + test('suggests an upgrade to the dart sdk', () async { + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + pubspec: { + 'environment': {'sdk': '>=2.18.0 <2.18.1'} + }, + ); + server.handle( + '/flutterReleases', + (request) => Response.ok(releasesMockResponse), + ); + await d.dir(appPath, [ + d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0') + ]).create(); + await pubGet( + error: contains('* Try using the Dart SDK version: 2.18.0'), + environment: { + '_PUB_TEST_SDK_VERSION': '2.17.0', + '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases', + }, + ); + }); + + test('suggests an upgrade or downgrade to a package constraint', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '^2.0.0'}); + server.serve('foo', '0.9.0', deps: {'bar': '^1.0.0'}); + + server.serve('bar', '1.0.0'); + server.serve('bar', '2.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '^1.0.0'}, + devDeps: {'bar': '^1.0.0'}, + ) + ]).create(); + await pubGet( + error: allOf( + [ + contains( + '* Consider downgrading your constraint on foo: dart pub add foo:^0.9.0', + ), + contains( + '* Try upgrading your constraint on bar: dart pub add dev:bar:^2.0.0', + ), + ], + ), + ); + }); + + test('suggests an update to an empty package constraint', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '>1.0.0 <=0.0.0'}, + ) + ]).create(); + await pubGet( + error: allOf( + [ + contains( + '* Try updating your constraint on foo: dart pub add foo:^1.0.0', + ), + ], + ), + ); + }); + + test('suggests updates to multiple packages', () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'}); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: {'foo': '1.0.0'}, + devDeps: {'bar': '1.0.0'}, + ) + ]).create(); + await pubGet( + error: contains( + '* Try updating the following constraints: dart pub add dev:bar:^2.0.0 foo:^2.0.0', + ), + ); + }); + + test('suggests a major upgrade if more than 5 needs to be upgraded', + () async { + final server = await servePackages(); + server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'}); + server.serve('foo1', '1.0.0', deps: {'bar1': '2.0.0'}); + server.serve('bar1', '1.0.0', deps: {'foo1': '2.0.0'}); + server.serve('foo1', '2.0.0', deps: {'bar1': '2.0.0'}); + server.serve('bar1', '2.0.0', deps: {'foo1': '2.0.0'}); + server.serve('foo2', '1.0.0', deps: {'bar2': '2.0.0'}); + server.serve('bar2', '1.0.0', deps: {'foo2': '2.0.0'}); + server.serve('foo2', '2.0.0', deps: {'bar2': '2.0.0'}); + server.serve('bar2', '2.0.0', deps: {'foo2': '2.0.0'}); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '1.0.0', + 'bar': '1.0.0', + 'foo1': '1.0.0', + 'bar1': '1.0.0', + 'foo2': '1.0.0', + 'bar2': '1.0.0', + }, + ) + ]).create(); + await pubGet( + error: contains( + '* Try an upgrade of your constraints: dart pub upgrade --major-versions', + ), + ); + }); + + test('suggests upgrades to non-default servers', () async { + final server = await servePackages(); + final server2 = await startPackageServer(); + server.serve( + 'foo', + '1.0.0', + deps: { + 'bar': {'version': '2.0.0', 'hosted': server2.url} + }, + ); + + server2.serve('bar', '1.0.0'); + server2.serve('bar', '2.0.0'); + + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '^1.0.0', + 'bar': {'version': '^1.0.0', 'hosted': server2.url}, + }, + ) + ]).create(); + await pubGet( + error: contains( + '* Try upgrading your constraint on bar: dart pub add ' + 'bar:\'{"version":"^2.0.0","hosted":"${server2.url}"}\'', + ), + ); + await pubAdd( + args: ['bar:{"version":"^2.0.0","hosted":"${server2.url}"}'], + ); + await d.dir(appPath, [ + d.libPubspec( + 'myApp', + '1.0.0', + deps: { + 'foo': '^1.0.0', + 'bar': {'version': '^2.0.0', 'hosted': server2.url}, + }, + ) + ]).validate(); + }); +} + +const releasesMockResponse = ''' +{ + "base_url": "https://storage.googleapis.com/flutter_infra_release/releases", + "current_release": { + "beta": "096162697a9cdc79f4e47f7230d70935fa81fd24", + "dev": "13a2fb10b838971ce211230f8ffdd094c14af02c", + "stable": "e3c29ec00c9c825c891d75054c63fcc46454dca1" + }, + "releases": [ + { + "hash": "e3c29ec00c9c825c891d75054c63fcc46454dca1", + "channel": "stable", + "version": "3.3.2", + "dart_sdk_version": "2.18.1", + "dart_sdk_arch": "x64", + "release_date": "2022-09-14T15:06:55.724077Z", + "archive": "stable/linux/flutter_linux_3.3.2-stable.tar.xz", + "sha256": "a733a75ae07c42b2059a31fc9d64fabfae5dccd15770fa6b7f290e3f5f9c98e8" + }, + { + "hash": "4f9d92fbbdf072a70a70d2179a9f87392b94104c", + "channel": "stable", + "version": "3.3.1", + "dart_sdk_version": "2.18.0", + "dart_sdk_arch": "x64", + "release_date": "2022-09-07T15:30:42.283999Z", + "archive": "stable/linux/flutter_linux_3.3.1-stable.tar.xz", + "sha256": "7cbcff0230affbe07a5ce82298044ac437e96aeba69f83656f9ed9a910a392e7" + }, + { + "hash": "ffccd96b62ee8cec7740dab303538c5fc26ac543", + "channel": "stable", + "version": "3.3.0", + "dart_sdk_version": "2.18.0", + "dart_sdk_arch": "x64", + "release_date": "2022-08-30T17:22:12.916008Z", + "archive": "stable/linux/flutter_linux_3.3.0-stable.tar.xz", + "sha256": "a92a27aa6d4454d7a1cf9f8a0a56e0e5d6865f2cfcd21cf52e57f7922ad5d504" + }, + { + "hash": "096162697a9cdc79f4e47f7230d70935fa81fd24", + "channel": "beta", + "version": "3.3.0-0.5.pre", + "dart_sdk_version": "2.18.0 (build 2.18.0-271.7.beta)", + "dart_sdk_arch": "x64", + "release_date": "2022-08-23T17:03:21.525151Z", + "archive": "beta/linux/flutter_linux_3.3.0-0.5.pre-beta.tar.xz", + "sha256": "8e07158a64a8ce79f9169cffe4ff23a486bdabb29401f13177672fae18de52d2" + } + ] +} +''';
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index 17c5be2..df3ae2b 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart
@@ -200,8 +200,8 @@ // Issue 1853 test( - "produces a nice message for a locked dependency that's the only " - 'version of its package', () async { + "produces a nice message for a locked dependency that's the only version of its package", + () async { await servePackages() ..serve('foo', '1.0.0', deps: {'bar': '>=2.0.0'}) ..serve('bar', '1.0.0') @@ -212,11 +212,9 @@ await d.appDir(dependencies: {'foo': 'any', 'bar': '<2.0.0'}).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because myapp depends on foo any which depends on bar >=2.0.0, - bar >=2.0.0 is required. - So, because myapp depends on bar <2.0.0, version solving failed. - '''), + error: contains(''' +Because myapp depends on foo any which depends on bar >=2.0.0, bar >=2.0.0 is required. +So, because myapp depends on bar <2.0.0, version solving failed.'''), ); }); } @@ -336,11 +334,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because no versions of foo match ^2.0.0 and myapp depends on foo - >=1.0.0 <3.0.0, foo ^1.0.0 is required. - So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed. - '''), + error: contains(''' +Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required. +So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''), ); }); @@ -357,11 +353,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because no versions of foo match ^2.0.0 and myapp depends on foo - >=1.0.0 <3.0.0, foo ^1.0.0 is required. - So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed. - '''), + error: contains(''' +Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required. +So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''), ); }); @@ -378,10 +372,9 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version - solving failed. - '''), + error: contains( + 'Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version solving failed.', + ), ); }); @@ -441,10 +434,8 @@ await d.appDir(dependencies: {'foo': '>=1.0.0 <2.0.0'}).create(); await expectResolves( - error: equalsIgnoringWhitespace(""" - Because myapp depends on foo ^1.0.0 which doesn't match any versions, - version solving failed. - """), + error: contains(''' +Because myapp depends on foo ^1.0.0 which doesn't match any versions, version solving failed.'''), ); }); @@ -575,11 +566,10 @@ ..serve('b', '1.0.0'); await d.appDir(dependencies: {'a': 'any', 'b': '>1.0.0'}).create(); + await expectResolves( - error: equalsIgnoringWhitespace(""" - Because myapp depends on b >1.0.0 which doesn't match any versions, - version solving failed. - """), + error: contains(''' +Because myapp depends on b >1.0.0 which doesn't match any versions, version solving failed.'''), ); }); @@ -1115,11 +1105,10 @@ ]).create(); await expectResolves( - error: equalsIgnoringWhitespace(''' - The current Dart SDK version is 3.1.2+3. + error: contains(''' +The current Dart SDK version is 3.1.2+3. - Because myapp requires SDK version 2.12.0, version solving failed. - '''), +Because myapp requires SDK version 2.12.0, version solving failed.'''), ); });