blob: 36408ec98d79345264233bc89d738762f73d3da5 [file] [log] [blame]
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;
}
}
}