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