| 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; | |
| } | |
| } | |
| } |