| 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_utils.dart'; |
| import '../solver.dart'; |
| import '../source/hosted.dart'; |
| import '../system_cache.dart'; |
| import 'incompatibility.dart'; |
| import 'incompatibility_cause.dart'; |
| |
| /// Looks through the root-[incompatibility] 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 { |
| if (entrypoint.workspaceRoot.workspaceChildren.isNotEmpty) { |
| // TODO(https://github.com/dart-lang/pub/issues/4227): handle workspaces. |
| return null; |
| } |
| 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 > const Duration(seconds: 3)) { |
| // Never spend more than 3 seconds computing suggestions. |
| break; |
| } |
| final cause = externalIncompatibility.cause; |
| if (cause is SdkIncompatibilityCause) { |
| 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.workspaceRoot.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.workspaceRoot.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( |
| SdkIncompatibilityCause 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 fulfilling the constraint. |
| final bestRelease = await inferBestFlutterRelease({ |
| cause.sdk.identifier: constraint, |
| }); |
| if (bestRelease == null) return null; |
| final result = await _tryResolve( |
| entrypoint.workspaceRoot, |
| 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 { |
| // TODO(https://github.com/dart-lang/pub/issues/4127): This should |
| // operate on all packages in workspace. |
| final originalRange = |
| entrypoint.workspaceRoot.dependencies[name] ?? |
| entrypoint.workspaceRoot.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.workspaceRoot.pubspec, |
| stripOnly: [name], |
| stripLowerBound: true, |
| ); |
| |
| final result = await _tryResolve( |
| Package( |
| relaxedPubspec, |
| entrypoint.workspaceRoot.dir, |
| entrypoint.workspaceRoot.workspaceChildren, |
| ), |
| ); |
| 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.workspaceRoot.pubspec; |
| final relaxedPubspec = stripVersionBounds( |
| originalPubspec, |
| stripLowerBound: stripLowerBound, |
| ); |
| |
| final result = await _tryResolve( |
| Package( |
| relaxedPubspec, |
| entrypoint.workspaceRoot.dir, |
| entrypoint.workspaceRoot.workspaceChildren, |
| ), |
| ); |
| 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( |
| Package package, { |
| Map<String, Version> sdkOverrides = const {}, |
| }) async { |
| try { |
| return await resolveVersions( |
| type, |
| cache, |
| package, |
| sdkOverrides: sdkOverrides, |
| lockFile: entrypoint.lockFile, |
| unlock: unlock, |
| ); |
| } on SolveFailure { |
| return null; |
| } |
| } |
| } |