Reinterpret dart sdk constraints when read from a lockfile (#3897)
* Reinterpret dart sdk constraints when read from a lockfile
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index eaa068c..93d6678 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -427,9 +427,15 @@
dataError('${log.bold(name)} as globally activated requires '
'unknown SDK "$name".');
} else if (sdkName == 'dart') {
- if (constraint.allows((sdk as DartSdk).version)) return;
- dataError("${log.bold(name)} as globally activated doesn't "
- 'support Dart ${sdk.version}, try: $topLevelProgram pub global activate $name');
+ if (constraint.effectiveConstraint.allows((sdk as DartSdk).version)) {
+ return;
+ }
+ dataError('''
+${log.bold(name)} as globally activated doesn't support Dart ${sdk.version}.
+
+try:
+`$topLevelProgram pub global activate $name` to reactivate.
+''');
} else {
dataError('${log.bold(name)} as globally activated requires the '
'${sdk.name} SDK, which is unsupported for global executables.');
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index bdafe4e..f725260 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -15,6 +15,7 @@
import 'language_version.dart';
import 'package_config.dart';
import 'package_name.dart';
+import 'pubspec.dart';
import 'sdk.dart' show sdk;
import 'system_cache.dart';
import 'utils.dart';
@@ -26,7 +27,7 @@
/// The intersections of all SDK constraints for all locked packages, indexed
/// by SDK identifier.
- Map<String, VersionConstraint> sdkConstraints;
+ Map<String, SdkConstraint> sdkConstraints;
/// Dependency names that appeared in the root package's `dependencies`
/// section.
@@ -49,7 +50,7 @@
/// analysis server to provide better auto-completion.
LockFile(
Iterable<PackageId> ids, {
- Map<String, VersionConstraint>? sdkConstraints,
+ Map<String, SdkConstraint>? sdkConstraints,
Set<String>? mainDependencies,
Set<String>? devDependencies,
Set<String>? overriddenDependencies,
@@ -58,7 +59,7 @@
ids.where((id) => !id.isRoot),
key: (id) => id.name,
),
- sdkConstraints ?? {'dart': VersionConstraint.any},
+ sdkConstraints ?? {'dart': SdkConstraint(VersionConstraint.any)},
mainDependencies ?? const UnmodifiableSetView.empty(),
devDependencies ?? const UnmodifiableSetView.empty(),
overriddenDependencies ?? const UnmodifiableSetView.empty(),
@@ -74,7 +75,7 @@
LockFile.empty()
: packages = const {},
- sdkConstraints = {'dart': VersionConstraint.any},
+ sdkConstraints = {'dart': SdkConstraint(VersionConstraint.any)},
mainDependencies = const UnmodifiableSetView.empty(),
devDependencies = const UnmodifiableSetView.empty(),
overriddenDependencies = const UnmodifiableSetView.empty();
@@ -114,14 +115,17 @@
'YAML mapping',
);
- final sdkConstraints = <String, VersionConstraint>{};
+ final sdkConstraints = <String, SdkConstraint>{};
final sdkNode =
_getEntry<YamlScalar?>(parsed, 'sdk', 'string', required: false);
if (sdkNode != null) {
// Lockfiles produced by pub versions from 1.14.0 through 1.18.0 included
// a top-level "sdk" field which encoded the unified constraint on the
// Dart SDK. They had no way of specifying constraints on other SDKs.
- sdkConstraints['dart'] = _parseVersionConstraint(sdkNode);
+ sdkConstraints['dart'] = SdkConstraint.interpretDartSdkConstraint(
+ _parseVersionConstraint(sdkNode),
+ defaultUpperBoundConstraint: null,
+ );
}
final sdksField =
@@ -131,7 +135,19 @@
_parseEachEntry<String, YamlScalar>(
sdksField,
(name, constraint) {
- sdkConstraints[name] = _parseVersionConstraint(constraint);
+ final originalConstraint = _parseVersionConstraint(constraint);
+ // Reinterpret the sdk constraints here, in case they were written by
+ // an old sdk that did not do reinterpretations.
+ // TODO(sigurdm): push the switching into `SdkConstraint`.
+ sdkConstraints[name] = switch (name) {
+ 'dart' => SdkConstraint.interpretDartSdkConstraint(
+ originalConstraint,
+ defaultUpperBoundConstraint: null,
+ ),
+ 'flutter' =>
+ SdkConstraint.interpretFlutterSdkConstraint(originalConstraint),
+ _ => SdkConstraint(originalConstraint),
+ };
},
'string',
'string',
@@ -411,7 +427,7 @@
var data = {
'sdks': mapMap(
sdkConstraints,
- value: (_, constraint) => constraint.toString(),
+ value: (_, constraint) => constraint.effectiveConstraint.toString(),
),
'packages': packageMap
};
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 5f27abc..c27fdb7 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -145,56 +145,6 @@
/// pubspec.
final bool _includeDefaultSdkConstraint;
- SdkConstraint _interpretDartSdkConstraint(
- VersionConstraint originalConstraint, {
- required VersionConstraint? defaultUpperBoundConstraint,
- }) {
- VersionConstraint constraint = originalConstraint;
- if (defaultUpperBoundConstraint != null &&
- constraint is VersionRange &&
- constraint.max == null &&
- defaultUpperBoundConstraint.allowsAny(constraint)) {
- constraint = VersionConstraint.intersection(
- [constraint, defaultUpperBoundConstraint],
- );
- }
- // If a package is null safe it should also be compatible with dart 3.
- // Therefore we rewrite a null-safety enabled constraint with the upper
- // bound <3.0.0 to be have upper bound <4.0.0
- //
- // Only do this rewrite after dart 3.
- if (sdk.version.major >= 3 &&
- constraint is VersionRange &&
- LanguageVersion.fromSdkConstraint(constraint) >=
- LanguageVersion.firstVersionWithNullSafety &&
- // <3.0.0 is parsed into a max of 3.0.0-0, so that is what we look for
- // here.
- constraint.max == Version(3, 0, 0).firstPreRelease &&
- constraint.includeMax == false) {
- constraint = VersionRange(
- min: constraint.min,
- includeMin: constraint.includeMin,
- // We don't have to use .firstPreRelease as the constructor will do that
- // if needed.
- max: Version(4, 0, 0),
- );
- }
- return SdkConstraint(constraint, originalConstraint: originalConstraint);
- }
-
- // Flutter constraints get special treatment, as Flutter won't be using
- // semantic versioning to mark breaking releases. We simply ignore upper
- // bounds.
- SdkConstraint _interpretFlutterSdkConstraint(VersionConstraint constraint) {
- if (constraint is VersionRange) {
- return SdkConstraint(
- VersionRange(min: constraint.min, includeMin: constraint.includeMin),
- originalConstraint: constraint,
- );
- }
- return SdkConstraint(constraint);
- }
-
/// Parses the "environment" field in [parent] and returns a map from SDK
/// identifiers to constraints on those SDKs.
Map<String, SdkConstraint> _parseEnvironment(YamlMap parent) {
@@ -215,7 +165,7 @@
);
}
var constraints = {
- 'dart': _interpretDartSdkConstraint(
+ 'dart': SdkConstraint.interpretDartSdkConstraint(
originalDartSdkConstraint,
defaultUpperBoundConstraint: _includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
@@ -239,7 +189,7 @@
_FileType.pubspec,
);
constraints[name] = name == 'flutter'
- ? _interpretFlutterSdkConstraint(constraint)
+ ? SdkConstraint.interpretFlutterSdkConstraint(constraint)
: SdkConstraint(constraint);
});
}
@@ -669,6 +619,62 @@
VersionConstraint? originalConstraint,
}) : originalConstraint = originalConstraint ?? effectiveConstraint;
+ /// Implement support for down to 2.12 in the dart 3 series. Note that this
+ /// function has to be be idempotent, because we apply it both when we write
+ /// and read lock-file constraints, so applying this function a second time
+ /// should have no further effect.
+ factory SdkConstraint.interpretDartSdkConstraint(
+ VersionConstraint originalConstraint, {
+ required VersionConstraint? defaultUpperBoundConstraint,
+ }) {
+ VersionConstraint constraint = originalConstraint;
+ if (defaultUpperBoundConstraint != null &&
+ constraint is VersionRange &&
+ constraint.max == null &&
+ defaultUpperBoundConstraint.allowsAny(constraint)) {
+ constraint = VersionConstraint.intersection(
+ [constraint, defaultUpperBoundConstraint],
+ );
+ }
+ // If a package is null safe it should also be compatible with dart 3.
+ // Therefore we rewrite a null-safety enabled constraint with the upper
+ // bound <3.0.0 to be have upper bound <4.0.0
+ //
+ // Only do this rewrite after dart 3.
+ if (sdk.version.major >= 3 &&
+ constraint is VersionRange &&
+ LanguageVersion.fromSdkConstraint(constraint) >=
+ LanguageVersion.firstVersionWithNullSafety &&
+ // <3.0.0 is parsed into a max of 3.0.0-0, so that is what we look for
+ // here.
+ constraint.max == Version(3, 0, 0).firstPreRelease &&
+ constraint.includeMax == false) {
+ constraint = VersionRange(
+ min: constraint.min,
+ includeMin: constraint.includeMin,
+ // We don't have to use .firstPreRelease as the constructor will do that
+ // if needed.
+ max: Version(4, 0, 0),
+ );
+ }
+ return SdkConstraint(constraint, originalConstraint: originalConstraint);
+ }
+
+ // Flutter constraints get special treatment, as Flutter won't be using
+ // semantic versioning to mark breaking releases. We simply ignore upper
+ // bounds.
+ factory SdkConstraint.interpretFlutterSdkConstraint(
+ VersionConstraint constraint,
+ ) {
+ if (constraint is VersionRange) {
+ return SdkConstraint(
+ VersionRange(min: constraint.min, includeMin: constraint.includeMin),
+ originalConstraint: constraint,
+ );
+ }
+ return SdkConstraint(constraint);
+ }
+
/// The language version of a constraint is determined from how it is written.
LanguageVersion get languageVersion =>
LanguageVersion.fromSdkConstraint(originalConstraint);
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index bb70ef1..8c5a33f 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -96,7 +96,10 @@
}
return LockFile(
resolvedPackageIds,
- sdkConstraints: sdkConstraints,
+ sdkConstraints: {
+ for (final MapEntry(:key, :value) in sdkConstraints.entries)
+ key: SdkConstraint(value),
+ },
mainDependencies: MapKeySet(_root.dependencies),
devDependencies: MapKeySet(_root.devDependencies),
overriddenDependencies: MapKeySet(_root.dependencyOverrides),
diff --git a/test/global/run/fails_if_sdk_constraint_is_unmet_test.dart b/test/global/run/fails_if_sdk_constraint_is_unmet_test.dart
index deb31a7..ec2e8c6 100644
--- a/test/global/run/fails_if_sdk_constraint_is_unmet_test.dart
+++ b/test/global/run/fails_if_sdk_constraint_is_unmet_test.dart
@@ -2,8 +2,12 @@
// 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:io';
+
+import 'package:path/path.dart' as p;
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
import '../../descriptor.dart' as d;
import '../../test_pub.dart';
@@ -110,9 +114,47 @@
environment: {'_PUB_TEST_SDK_VERSION': '3.0.0'},
args: ['global', 'run', 'foo:script'],
error: contains(
- "foo as globally activated doesn't support Dart 3.0.0, try: dart pub global activate foo",
+ """
+foo as globally activated doesn't support Dart 3.0.0.
+
+try:
+`dart pub global activate foo` to reactivate.""",
),
exitCode: exit_codes.DATA,
);
});
+
+ test('succeeds if SDK is upgraded from 2.19 to 3.0', () async {
+ final server = await servePackages();
+ server.serve(
+ 'foo',
+ '1.0.0',
+ sdk: '^2.19.0',
+ contents: [
+ d.dir(
+ 'bin',
+ [d.file('script.dart', "main(args) => print('123-OK');")],
+ )
+ ],
+ );
+
+ await runPub(
+ environment: {'_PUB_TEST_SDK_VERSION': '2.19.0'},
+ args: ['global', 'activate', 'foo'],
+ );
+
+ final lockFile = File(
+ p.join(d.sandbox, cachePath, 'global_packages', 'foo', 'pubspec.lock'),
+ );
+ final editor = YamlEditor(lockFile.readAsStringSync());
+ // This corresponds to what an older sdk would write, before the dart 3 hack
+ // was introduced:
+ editor.update(['sdks', 'dart'], '^2.19.0');
+
+ await runPub(
+ environment: {'_PUB_TEST_SDK_VERSION': '3.0.0'},
+ args: ['global', 'run', 'foo:script'],
+ output: contains('123-OK'),
+ );
+ });
}
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart
index ce76003..3dbcf92 100644
--- a/test/lock_file_test.dart
+++ b/test/lock_file_test.dart
@@ -94,8 +94,8 @@
test('allows an old-style SDK constraint', () {
var lockFile = LockFile.parse('sdk: ">=1.2.3 <4.0.0"', sources);
expect(
- lockFile.sdkConstraints,
- containsPair('dart', VersionConstraint.parse('>=1.2.3 <4.0.0')),
+ lockFile.sdkConstraints['dart']!.effectiveConstraint,
+ VersionConstraint.parse('>=1.2.3 <4.0.0'),
);
expect(lockFile.sdkConstraints, isNot(contains('flutter')));
expect(lockFile.sdkConstraints, isNot(contains('fuchsia')));
@@ -112,16 +112,16 @@
sources,
);
expect(
- lockFile.sdkConstraints,
- containsPair('dart', VersionConstraint.parse('>=1.2.3 <4.0.0')),
+ lockFile.sdkConstraints['dart']!.effectiveConstraint,
+ VersionConstraint.parse('>=1.2.3 <4.0.0'),
);
expect(
- lockFile.sdkConstraints,
- containsPair('flutter', VersionConstraint.parse('^0.1.2')),
+ lockFile.sdkConstraints['flutter']!.effectiveConstraint,
+ VersionConstraint.parse('>=0.1.2'),
);
expect(
- lockFile.sdkConstraints,
- containsPair('fuchsia', VersionConstraint.parse('^5.6.7')),
+ lockFile.sdkConstraints['fuchsia']!.effectiveConstraint,
+ VersionConstraint.parse('^5.6.7'),
);
});