| // Copyright (c) 2017, 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:collection'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| import '../experiment.dart'; | |
| import '../lock_file.dart'; | |
| import '../log.dart' as log; | |
| import '../package.dart'; | |
| import '../package_name.dart'; | |
| import '../pubspec.dart'; | |
| import '../sdk.dart'; | |
| import '../source/cached.dart'; | |
| import '../system_cache.dart'; | |
| import '../utils.dart'; | |
| /// The result of a successful version resolution. | |
| class SolveResult { | |
| /// The list of concrete package versions that were selected for each package | |
| /// reachable from the root. | |
| final List<PackageId> packages; | |
| /// Names of all dependency overrides in the workspace. | |
| final Set<String> _overriddenPackages; | |
| /// The root package of this resolution. | |
| final Package _root; | |
| /// A map from package names to the pubspecs for the versions of those | |
| /// packages that were installed. | |
| final Map<String, Pubspec> pubspecs; | |
| /// The available versions of all selected packages from their source. | |
| /// | |
| /// An entry here may not include the full list of versions available if the | |
| /// given package was locked and did not need to be unlocked during the solve. | |
| /// | |
| /// No version list will not contain any retracted package versions. | |
| final Map<String, List<Version>> availableVersions; | |
| /// The number of solutions that were attempted before either finding a | |
| /// successful solution or exhausting all options. | |
| /// | |
| /// In other words, one more than the number of times it had to backtrack | |
| /// because it found an invalid solution. | |
| final int attemptedSolutions; | |
| /// The wall clock time the resolution took. | |
| final Duration resolutionTime; | |
| /// The experiments that packages in this solve are allowed to use. | |
| /// | |
| /// That is the union of the experiments listed by the workspace packages. | |
| List<String> get experiments => _root.allExperimentsInWorkspace.toList(); | |
| /// All experiments known in this resolution (both SDK experiments and | |
| /// experiments declared by resolved packages), keyed by their qualified | |
| /// name (`<sdk-experiment>` or `<package>.<experiment>`). | |
| late final Map<String, Experiment> resolvedExperiments = { | |
| ...availableExperiments, | |
| for (final pubspec in pubspecs.values) | |
| for (final MapEntry(:key, :value) in pubspec.declaredExperiments.entries) | |
| '${pubspec.name}.$key': value, | |
| }; | |
| /// For each experiment that some package in the solution opts in to or out | |
| /// of, the sorted names of those packages. | |
| /// | |
| /// Experiments that have no effect are left out. | |
| Map<String, List<String>> get experimentUsers { | |
| final result = SplayTreeMap<String, List<String>>(); | |
| for (final name in pubspecs.keys.sorted()) { | |
| for (final experiment in pubspecs[name]!.experiments) { | |
| if (!isEffectiveExperimentFlag( | |
| experiment, | |
| experiments: resolvedExperiments, | |
| )) { | |
| continue; | |
| } | |
| (result[experiment] ??= []).add(name); | |
| } | |
| } | |
| return result; | |
| } | |
| /// For each expired experiment that some workspace package lists, the | |
| /// sorted names of those packages. | |
| Map<String, List<String>> get expiredExperimentUsers { | |
| final result = SplayTreeMap<String, List<String>>(); | |
| for (final package in _root.transitiveWorkspace.sortedBy((p) => p.name)) { | |
| for (final experiment in package.pubspec.experiments) { | |
| final experimentName = | |
| experiment.startsWith('no-') ? experiment.substring(3) : experiment; | |
| if (resolvedExperiments[experimentName]?.expired == true) { | |
| (result[experiment] ??= []).add(package.name); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| /// Validates that every package-scoped experiment (`<package>.<experiment>`) | |
| /// listed by a workspace package refers to a package in [pubspecs] that | |
| /// declares `<experiment>`. | |
| void validatePackageExperiments() { | |
| for (final package in _root.transitiveWorkspace.sortedBy((p) => p.name)) { | |
| for (final experiment in package.pubspec.experiments) { | |
| final dotIndex = experiment.indexOf('.'); | |
| if (dotIndex == -1) continue; | |
| final targetPackage = experiment.substring(0, dotIndex); | |
| final experimentName = experiment.substring(dotIndex + 1); | |
| final targetPubspec = pubspecs[targetPackage]; | |
| if (targetPubspec == null) { | |
| dataError( | |
| 'The experiment `$experiment` in the pubspec.yaml of ' | |
| '${package.name} refers to package `$targetPackage`, which is not ' | |
| 'in the dependency graph.', | |
| ); | |
| } | |
| final declared = targetPubspec.declaredExperiments[experimentName]; | |
| if (declared == null) { | |
| final available = | |
| targetPubspec.declaredExperiments.values | |
| .where((e) => !e.expired) | |
| .toList(); | |
| final availableDescription = | |
| available.isEmpty | |
| ? 'Package `$targetPackage` (${targetPubspec.version}) does ' | |
| 'not declare any experiments.' | |
| : ''' | |
| Available experiments in `$targetPackage` (${targetPubspec.version}) are: | |
| ${available.map((e) => '* ${e.summary}').join('\n')}'''; | |
| dataError(''' | |
| `$experiment` is not a known experiment of package `$targetPackage` (${targetPubspec.version}). | |
| $availableDescription'''); | |
| } | |
| } | |
| } | |
| } | |
| /// Downloads all the cached packages selected by this version resolution. | |
| /// | |
| /// If some already cached package differs from what is provided by the server | |
| /// (according to the content-hash) a warning is printed and the package is | |
| /// redownloaded. | |
| /// | |
| /// Returns the [LockFile] representing the packages selected by this version | |
| /// resolution. Any resolved [PackageId]s will correspond to those in the | |
| /// cache (and thus to the one provided by the server). | |
| /// | |
| /// If there is a mismatch between the previous content-hash from pubspec.lock | |
| /// and the new one a warning will be printed but the new one will be | |
| /// returned. | |
| Future<LockFile> downloadCachedPackages( | |
| SystemCache cache, { | |
| bool transient = false, | |
| }) async { | |
| final resolvedPackageIds = await log.progress<List<PackageId>>( | |
| 'Downloading packages', | |
| () => Future.wait( | |
| packages.map((id) async { | |
| if (id.source is CachedSource) { | |
| return (await cache.downloadPackage(id)).packageId; | |
| } | |
| return id; | |
| }), | |
| ), | |
| transient: transient, | |
| ); | |
| // Invariant: the content-hashes in PUB_CACHE matches those provided by the | |
| // server. | |
| // Don't factor in overridden dependencies' SDK constraints, because we'll | |
| // accept those packages even if their constraints don't match. | |
| final nonOverrides = | |
| pubspecs.values | |
| .where((pubspec) => !_overriddenPackages.contains(pubspec.name)) | |
| .toList(); | |
| final sdkConstraints = <String, VersionConstraint>{}; | |
| for (var pubspec in nonOverrides) { | |
| pubspec.sdkConstraints.forEach((identifier, constraint) { | |
| sdkConstraints[identifier] = constraint.effectiveConstraint.intersect( | |
| sdkConstraints[identifier] ?? VersionConstraint.any, | |
| ); | |
| }); | |
| } | |
| return LockFile( | |
| resolvedPackageIds, | |
| sdkConstraints: { | |
| for (final MapEntry(:key, :value) in sdkConstraints.entries) | |
| key: SdkConstraint(value), | |
| }, | |
| mainDependencies: MapKeySet(_root.dependencies), | |
| devDependencies: MapKeySet(_root.devDependencies), | |
| overriddenDependencies: _overriddenPackages, | |
| ); | |
| } | |
| final LockFile _previousLockFile; | |
| /// Returns the names of all packages that were changed. | |
| /// | |
| /// This includes packages that were added or removed. | |
| Set<String> get changedPackages { | |
| final changed = | |
| packages | |
| .where((id) => _previousLockFile.packages[id.name] != id) | |
| .map((id) => id.name) | |
| .toSet(); | |
| return changed.union( | |
| _previousLockFile.packages.keys | |
| .where((package) => !availableVersions.containsKey(package)) | |
| .toSet(), | |
| ); | |
| } | |
| SolveResult( | |
| this._root, | |
| this._overriddenPackages, | |
| this._previousLockFile, | |
| this.packages, | |
| this.pubspecs, | |
| this.availableVersions, | |
| this.attemptedSolutions, | |
| this.resolutionTime, | |
| ); | |
| @override | |
| String toString() => | |
| 'Took $attemptedSolutions tries to resolve to\n' | |
| '- ${packages.join("\n- ")}'; | |
| } |