| // Copyright (c) 2012, 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:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:math'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:pool/pool.dart'; | |
| import 'package:pub_semver/pub_semver.dart'; | |
| import 'package:source_span/source_span.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import 'package:yaml_edit/yaml_edit.dart'; | |
| import 'command_runner.dart'; | |
| import 'dart.dart' as dart; | |
| import 'exceptions.dart'; | |
| import 'executable.dart'; | |
| import 'experiment.dart'; | |
| import 'io.dart'; | |
| import 'language_version.dart'; | |
| import 'lock_file.dart'; | |
| import 'log.dart' as log; | |
| import 'package.dart'; | |
| import 'package_config.dart'; | |
| import 'package_graph.dart'; | |
| import 'package_name.dart'; | |
| import 'path.dart'; | |
| import 'platform_info.dart'; | |
| import 'pubspec.dart'; | |
| import 'pubspec_utils.dart'; | |
| import 'sdk.dart'; | |
| import 'sdk/flutter.dart'; | |
| import 'solver.dart'; | |
| import 'solver/report.dart'; | |
| import 'solver/solve_suggestions.dart'; | |
| import 'solver/version_solver.dart'; | |
| import 'source/cached.dart'; | |
| import 'source/hosted.dart'; | |
| import 'source/root.dart'; | |
| import 'source/unknown.dart'; | |
| import 'system_cache.dart'; | |
| import 'utils.dart'; | |
| /// The context surrounding the workspace pub is operating on. | |
| /// | |
| /// Pub operates over a directed graph of dependencies that starts at a root | |
| /// "entrypoint" package. This is typically the package where the current | |
| /// working directory is located. | |
| /// | |
| /// An entrypoint knows the [workspaceRoot] package it is associated with and is | |
| /// responsible for managing the package config (.dart_tool/package_config.json) | |
| /// and lock file (pubspec.lock) for it. | |
| /// | |
| /// While entrypoints are typically applications, a pure library package may end | |
| /// up being used as an entrypoint while under development. Also, a single | |
| /// package may be used as an entrypoint in one context but not in another. For | |
| /// example, a package that contains a reusable library may not be the | |
| /// entrypoint when used by an app, but may be the entrypoint when you're | |
| /// running its tests. | |
| class Entrypoint { | |
| /// The directory where this entrypoint is created. | |
| /// | |
| /// [workspaceRoot] will be the package in the nearest parent directory that | |
| /// has `resolution: null` | |
| final String workingDir; | |
| /// Finds the [workspaceRoot] and [workPackage] based on [workingDir]. | |
| /// | |
| /// Works by iterating through the parent directories from [workingDir]. | |
| /// | |
| /// [workPackage] is the package of first dir we find with a `pubspec.yaml` | |
| /// file. | |
| /// | |
| /// [workspaceRoot] is the package of the first dir we find with a | |
| /// `pubspec.yaml` that does not have `resolution: workspace`. | |
| /// | |
| /// [workPackage] and [workspaceRoot] can be the same. And will always be the | |
| /// same when no `workspace` is involved. | |
| /// = | |
| /// If [workingDir] doesn't exist, [fail]. | |
| /// | |
| /// If no `pubspec.yaml` is found without `resolution: workspace` we [fail]. | |
| static ({Package root, Package work}) _loadWorkspace( | |
| String workingDir, | |
| SystemCache cache, | |
| ) { | |
| if (!dirExists(workingDir)) { | |
| fail('The directory `$workingDir` does not exist.'); | |
| } | |
| // Keep track of all the pubspecs met when walking up the file system. | |
| // The first of these is the workingPackage. | |
| final pubspecsMet = <String, Pubspec>{}; | |
| for (final dir in parentDirs(workingDir)) { | |
| final Pubspec pubspec; | |
| try { | |
| pubspec = Pubspec.load( | |
| dir, | |
| cache.sources, | |
| containingDescription: ResolvedRootDescription.fromDir(dir), | |
| allowOverridesFile: true, | |
| ); | |
| } on FileException { | |
| continue; | |
| } | |
| pubspecsMet[p.canonicalize(dir)] = pubspec; | |
| final Package root; | |
| if (pubspec.resolution == Resolution.none) { | |
| root = Package.load( | |
| dir, | |
| loadPubspec: | |
| (path, {expectedName, required withPubspecOverrides}) => | |
| pubspecsMet[p.canonicalize(path)] ?? | |
| Pubspec.load( | |
| path, | |
| cache.sources, | |
| expectedName: expectedName, | |
| allowOverridesFile: withPubspecOverrides, | |
| containingDescription: ResolvedRootDescription.fromDir( | |
| path, | |
| ), | |
| ), | |
| withPubspecOverrides: true, | |
| ); | |
| for (final package in root.transitiveWorkspace) { | |
| if (identical(pubspecsMet.entries.first.value, package.pubspec)) { | |
| validateWorkspace(root); | |
| return (root: root, work: package); | |
| } | |
| } | |
| assert(false); | |
| } | |
| } | |
| if (pubspecsMet.isEmpty) { | |
| final dir = p.normalize(p.absolute(workingDir)); | |
| throw FileException( | |
| 'Found no `pubspec.yaml` file in `$dir` or parent directories', | |
| p.join(workingDir, 'pubspec.yaml'), | |
| ); | |
| } else { | |
| final firstEntry = pubspecsMet.entries.first; | |
| throw FileException( | |
| ''' | |
| Found a pubspec.yaml at ${firstEntry.key}. But it has resolution `${firstEntry.value.resolution.name}`. | |
| But found no workspace root including it in parent directories. | |
| See $workspacesDocUrl for more information.''', | |
| p.join(workingDir, 'pubspec.yaml'), | |
| ); | |
| } | |
| } | |
| /// Stores the result of [_loadWorkspace]. | |
| /// Only access via [workspaceRoot], [workPackage] and [canFindWorkspaceRoot]. | |
| ({Package root, Package work})? _packages; | |
| /// Only access via [workspaceRoot], [workPackage] and [canFindWorkspaceRoot]. | |
| ({Package root, Package work}) get _getPackages => | |
| _packages ??= _loadWorkspace(workingDir, cache); | |
| /// The root package this entrypoint is associated with. | |
| /// | |
| /// For a global package, this is the activated package. | |
| Package get workspaceRoot => _getPackages.root; | |
| /// True if we can find a `pubspec.yaml` to resolve in [workingDir] or any | |
| /// parent directory. | |
| bool get canFindWorkspaceRoot { | |
| try { | |
| _getPackages; | |
| return true; | |
| } on FileException { | |
| return false; | |
| } | |
| } | |
| /// The "focus" package that the current command should act upon. | |
| /// | |
| /// It will be the package in the nearest parent directory to `workingDir`. | |
| /// Example: if a workspace looks like this: | |
| /// | |
| /// foo/ pubspec.yaml # contains `workspace: [- 'bar'] bar/ pubspec.yaml # | |
| /// contains `resolution: workspace`. | |
| /// | |
| /// Running `pub add` in `foo/bar` will have bar as workPackage, and add | |
| /// dependencies to `foo/bar/pubspec.yaml`. | |
| /// | |
| /// Running `pub add` in `foo` will have foo as workPackage, and add | |
| /// dependencies to `foo/pubspec.yaml`. | |
| Package get workPackage => _getPackages.work; | |
| /// The system-wide cache which caches packages that need to be fetched over | |
| /// the network. | |
| final SystemCache cache; | |
| /// Whether this entrypoint exists within the package cache. | |
| bool get isCached => p.isWithin(cache.rootDir, workingDir); | |
| /// Whether this is an entrypoint for a globally-activated package. | |
| /// | |
| /// False for path-activated global packages. | |
| final bool isCachedGlobal; | |
| /// The lockfile for the entrypoint. | |
| /// | |
| /// If not provided to the entrypoint, it will be loaded lazily from disk. | |
| LockFile get lockFile => _lockFile ??= _loadLockFile(lockFilePath, cache); | |
| static LockFile _loadLockFile(String lockFilePath, SystemCache cache) { | |
| if (!fileExists(lockFilePath)) { | |
| return LockFile.empty(); | |
| } else { | |
| try { | |
| return LockFile.load(lockFilePath, cache.sources); | |
| } on SourceSpanException catch (e) { | |
| throw SourceSpanApplicationException( | |
| e.message, | |
| e.span, | |
| explanation: 'Failed parsing lock file:', | |
| hint: | |
| 'Consider deleting the file and running ' | |
| '`$topLevelProgram pub get` to recreate it.', | |
| ); | |
| } | |
| } | |
| } | |
| LockFile? _lockFile; | |
| /// The `.dart_tool/package_config.json` package-config of this entrypoint. | |
| /// | |
| /// Lazily initialized. Will throw [DataException] when initializing if the | |
| /// `.dart_tool/packageConfig.json` file doesn't exist or has a bad format . | |
| PackageConfig get packageConfig => | |
| _packageConfig ??= _loadPackageConfig(packageConfigPath); | |
| PackageConfig? _packageConfig; | |
| static PackageConfig _loadPackageConfig(String packageConfigPath) { | |
| Never badPackageConfig() { | |
| dataError( | |
| 'The "$packageConfigPath" file is not recognized by ' | |
| '"pub" version, please run "$topLevelProgram pub get".', | |
| ); | |
| } | |
| final String packageConfigRaw; | |
| try { | |
| packageConfigRaw = readTextFile(packageConfigPath); | |
| } on FileException { | |
| dataError( | |
| 'The "$packageConfigPath" file does not exist, ' | |
| 'please run "$topLevelProgram pub get".', | |
| ); | |
| } | |
| final PackageConfig result; | |
| try { | |
| result = PackageConfig.fromJson(json.decode(packageConfigRaw) as Object?); | |
| } on FormatException { | |
| badPackageConfig(); | |
| } | |
| // Version 2 is the initial version number for `package_config.json`, | |
| // because `.packages` was version 1 (even if it was a different file). | |
| // If the version is different from 2, then it must be a newer incompatible | |
| // version, hence, the user should run `pub get` with the downgraded SDK. | |
| if (result.configVersion != 2) { | |
| badPackageConfig(); | |
| } | |
| return result; | |
| } | |
| /// The package graph for the application and all of its transitive | |
| /// dependencies. | |
| /// | |
| /// Throws a [DataException] if the `.dart_tool/package_config.json` file | |
| /// isn't up-to-date relative to the pubspec and the lockfile. | |
| Future<PackageGraph> get packageGraph => | |
| _packageGraph ??= _createPackageGraph(); | |
| Future<PackageGraph> _createPackageGraph() async { | |
| // TODO(sigurdm): consider having [ensureUptoDate] and [acquireDependencies] | |
| // return the package-graph, such it by construction will always made from | |
| // an up-to-date package-config. | |
| await ensureUpToDate(workspaceRoot.dir, cache: cache); | |
| final packages = { | |
| for (var packageEntry in packageConfig.packages) | |
| packageEntry.name: Package.load( | |
| packageEntry.resolvedRootDir(packageConfigPath), | |
| expectedName: packageEntry.name, | |
| loadPubspec: Pubspec.loadRootWithSources(cache.sources), | |
| ), | |
| }; | |
| packages[workspaceRoot.name] = workspaceRoot; | |
| return PackageGraph(this, packages); | |
| } | |
| Future<PackageGraph>? _packageGraph; | |
| /// The path to the entrypoint's ".dart_tool/package_config.json" file | |
| /// relative to the current working directory . | |
| late final String packageConfigPath = p.relative( | |
| p.normalize(p.join(workspaceRoot.dir, '.dart_tool', 'package_config.json')), | |
| ); | |
| late final String packageGraphPath = p.relative( | |
| p.normalize(p.join(workspaceRoot.dir, '.dart_tool', 'package_graph.json')), | |
| ); | |
| /// The path to the entrypoint workspace's lockfile. | |
| String get lockFilePath => | |
| p.normalize(p.join(workspaceRoot.dir, 'pubspec.lock')); | |
| /// The path to the directory containing global executable snapshots. | |
| String get _snapshotPath { | |
| assert(isCachedGlobal); | |
| return p.join(workspaceRoot.dir, 'bin'); | |
| } | |
| Entrypoint._( | |
| this.workingDir, | |
| this._lockFile, | |
| this._examples, | |
| this._packageGraph, | |
| this.cache, | |
| this._packages, | |
| this.isCachedGlobal, | |
| ); | |
| /// An entrypoint for the workspace containing [workingDir]/ | |
| /// | |
| /// If [checkInCache] is `true` (the default) an error will be thrown if | |
| /// [workingDir] is located inside [cache]`.rootDir`. | |
| Entrypoint(this.workingDir, this.cache, {bool checkInCache = true}) | |
| : isCachedGlobal = false { | |
| if (checkInCache && p.isWithin(cache.rootDir, workingDir)) { | |
| fail('Cannot operate on packages inside the cache.'); | |
| } | |
| } | |
| /// Creates an entrypoint at the same location, but with each pubspec in | |
| /// [updatedPubspecs] replacing the with one for the corresponding package. | |
| Entrypoint withUpdatedRootPubspecs(Map<Package, Pubspec> updatedPubspecs) { | |
| final newWorkspaceRoot = workspaceRoot.transformWorkspace( | |
| (package) => updatedPubspecs[package] ?? package.pubspec, | |
| ); | |
| final newWorkPackage = newWorkspaceRoot.transitiveWorkspace.firstWhere( | |
| (package) => package.dir == workPackage.dir, | |
| ); | |
| return Entrypoint._( | |
| workingDir, | |
| _lockFile, | |
| _examples, | |
| _packageGraph, | |
| cache, | |
| (root: newWorkspaceRoot, work: newWorkPackage), | |
| isCachedGlobal, | |
| ); | |
| } | |
| /// Creates an entrypoint at the same location, that will use [pubspec] for | |
| /// resolution of the [workPackage]. | |
| Entrypoint withWorkPubspec(Pubspec pubspec) { | |
| return withUpdatedRootPubspecs({workPackage: pubspec}); | |
| } | |
| /// Creates an entrypoint given package and lockfile objects. | |
| /// If a SolveResult is already created it can be passed as an optimization. | |
| Entrypoint.global( | |
| Package package, | |
| this._lockFile, | |
| this.cache, { | |
| SolveResult? solveResult, | |
| }) : _packages = (root: package, work: package), | |
| workingDir = package.dir, | |
| isCachedGlobal = true { | |
| if (solveResult != null) { | |
| _packageGraph = Future.value( | |
| PackageGraph.fromSolveResult(this, solveResult), | |
| ); | |
| } | |
| } | |
| /// Gets [Entrypoint]s for examples of any workspace packages. | |
| /// | |
| /// Does not return examples that are already in the workspace | |
| /// | |
| /// This will be empty if the example folder doesn't have a `pubspec.yaml`. | |
| List<Entrypoint> get examples { | |
| if (_examples case final List<Entrypoint> examples) return examples; | |
| final directoriesInWorkspace = <String>{}; | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| directoriesInWorkspace.add(p.canonicalize(package.dir)); | |
| } | |
| final result = <Entrypoint>[]; | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| final examplePath = package.path('example'); | |
| if (!directoriesInWorkspace.contains(p.canonicalize(examplePath)) && | |
| fileExists(p.join(examplePath, 'pubspec.yaml'))) { | |
| result.add(Entrypoint(examplePath, cache)); | |
| } | |
| } | |
| return _examples = result; | |
| } | |
| List<Entrypoint>? _examples; | |
| /// Writes the .dart_tool/package_config.json file and workspace references to | |
| /// it. | |
| /// | |
| /// Compares it to the existing .dart_tool/package_config.json and does not | |
| /// rewrite it unless it is | |
| /// | |
| /// Also writes the .dart_tool.package_graph.json file. | |
| /// | |
| /// If the workspace is non-trivial: For each package in the workspace write: | |
| /// `.dart_tool/pub/workspace_ref.json` with a pointer to the workspace root | |
| /// package dir. | |
| /// | |
| /// Also marks the package active in `PUB_CACHE/active_roots/`. | |
| Future<void> writePackageConfigFiles() async { | |
| ensureDir(p.dirname(packageConfigPath)); | |
| writeTextFileIfDifferent( | |
| packageConfigPath, | |
| await _packageConfigFile( | |
| cache, | |
| entrypointSdkConstraint: | |
| workspaceRoot | |
| .pubspec | |
| .sdkConstraints[sdk.identifier] | |
| ?.effectiveConstraint, | |
| ), | |
| dependencies: [lockFilePath], | |
| ); | |
| writeTextFileIfDifferent(packageGraphPath, await _packageGraphFile(cache)); | |
| if (workspaceRoot.workspaceChildren.isNotEmpty) { | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| final workspaceRefDir = p.join(package.dir, '.dart_tool', 'pub'); | |
| final workspaceRefPath = p.join(workspaceRefDir, 'workspace_ref.json'); | |
| ensureDir(workspaceRefDir); | |
| final relativeRootPath = p.relative( | |
| workspaceRoot.dir, | |
| from: workspaceRefDir, | |
| ); | |
| final workspaceRef = const JsonEncoder.withIndent( | |
| ' ', | |
| ).convert({'workspaceRoot': relativeRootPath}); | |
| writeTextFileIfDifferent(workspaceRefPath, '$workspaceRef\n'); | |
| } | |
| } | |
| if (lockFile.packages.values.any((id) => id.source is CachedSource)) { | |
| cache.markRootActive(packageConfigPath); | |
| } | |
| } | |
| Iterable<String> _resolutionFileDependencies() sync* { | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| yield package.pubspecPath; | |
| yield package.pubspecOverridesPath; | |
| } | |
| } | |
| Future<String> _packageGraphFile(SystemCache cache) async { | |
| return const JsonEncoder.withIndent(' ').convert({ | |
| 'roots': | |
| workspaceRoot.transitiveWorkspace.map((p) => p.name).toList()..sort(), | |
| 'packages': [ | |
| for (final p in workspaceRoot.transitiveWorkspace) | |
| { | |
| 'name': p.name, | |
| 'version': p.version.toString(), | |
| 'dependencies': p.dependencies.keys.toList()..sort(), | |
| 'devDependencies': p.devDependencies.keys.toList()..sort(), | |
| }, | |
| for (final p in lockFile.packages.values) | |
| { | |
| 'name': p.name, | |
| 'version': p.version.toString(), | |
| 'dependencies': | |
| (await cache.describe(p)).dependencies.keys.toList()..sort(), | |
| }, | |
| ], | |
| 'configVersion': 1, | |
| }); | |
| } | |
| /// Returns the contents of the `.dart_tool/package_config` file generated | |
| /// from this entrypoint based on [lockFile]. | |
| /// | |
| /// Each entry lists the experiments its package opts in to, see | |
| /// [Pubspec.experiments]. | |
| /// | |
| /// If [isCachedGlobal] no entry will be created for [workspaceRoot]. | |
| Future<String> _packageConfigFile( | |
| SystemCache cache, { | |
| VersionConstraint? entrypointSdkConstraint, | |
| }) async { | |
| final lockedPubspecs = <String, Pubspec>{}; | |
| if (lockFile.packages.isNotEmpty) { | |
| for (final name in lockFile.packages.keys.sorted()) { | |
| final id = lockFile.packages[name]!; | |
| lockedPubspecs[name] = await cache.describe(id); | |
| } | |
| } | |
| final resolvedExperiments = <String, Experiment>{ | |
| ...availableExperiments, | |
| for (final pubspec in lockedPubspecs.values) | |
| for (final MapEntry(:key, :value) | |
| in pubspec.declaredExperiments.entries) | |
| '${pubspec.name}.$key': value, | |
| if (!isCachedGlobal) | |
| for (final package in workspaceRoot.transitiveWorkspace) | |
| for (final MapEntry(:key, :value) | |
| in package.pubspec.declaredExperiments.entries) | |
| '${package.name}.$key': value, | |
| }; | |
| final entries = <PackageConfigEntry>[]; | |
| if (lockFile.packages.isNotEmpty) { | |
| final relativeFromPath = p.join(workspaceRoot.dir, '.dart_tool'); | |
| for (final name in lockFile.packages.keys.sorted()) { | |
| final id = lockFile.packages[name]!; | |
| final rootPath = cache.getDirectory(id, relativeFrom: relativeFromPath); | |
| final pubspec = lockedPubspecs[name]!; | |
| entries.add( | |
| PackageConfigEntry( | |
| name: name, | |
| rootUri: p.toUri(rootPath), | |
| packageUri: p.toUri('lib/'), | |
| languageVersion: pubspec.languageVersion, | |
| experiments: _experimentsNeedingOptIn(pubspec, resolvedExperiments), | |
| ), | |
| ); | |
| } | |
| } | |
| if (!isCachedGlobal) { | |
| /// Run through the entire workspace transitive closure and add an entry | |
| /// for each package. | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| entries.add( | |
| PackageConfigEntry( | |
| name: package.name, | |
| rootUri: p.toUri( | |
| p.relative( | |
| package.dir, | |
| from: p.join(workspaceRoot.dir, '.dart_tool'), | |
| ), | |
| ), | |
| packageUri: p.toUri('lib/'), | |
| languageVersion: package.pubspec.languageVersion, | |
| experiments: _experimentsNeedingOptIn( | |
| package.pubspec, | |
| resolvedExperiments, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| final packageConfig = PackageConfig( | |
| configVersion: 2, | |
| packages: entries, | |
| generator: 'pub', | |
| generatorVersion: sdk.version, | |
| additionalProperties: { | |
| if (FlutterSdk().isAvailable) ...{ | |
| 'flutterRoot': | |
| p.toUri(p.absolute(FlutterSdk().rootDirectory!)).toString(), | |
| 'flutterVersion': FlutterSdk().version.toString(), | |
| }, | |
| 'pubCache': p.toUri(p.absolute(cache.rootDir)).toString(), | |
| }, | |
| ); | |
| final jsonText = const JsonEncoder.withIndent( | |
| ' ', | |
| ).convert(packageConfig.toJson()); | |
| return '$jsonText\n'; | |
| } | |
| /// The experiments of [pubspec] that tools must be told about. | |
| /// | |
| /// Experiments that don't require opting in (or can't be disabled) are left | |
| /// out. | |
| static List<String> _experimentsNeedingOptIn( | |
| Pubspec pubspec, | |
| Map<String, Experiment> resolvedExperiments, | |
| ) => [ | |
| for (final name in pubspec.experiments) | |
| if (isEffectiveExperimentFlag(name, experiments: resolvedExperiments)) | |
| name, | |
| ]; | |
| /// Gets all dependencies of the [workspaceRoot] package. | |
| /// | |
| /// Performs version resolution according to [SolveType]. | |
| /// | |
| /// The iterable [unlock] specifies the list of packages whose versions can be | |
| /// changed even if they are locked in the pubspec.lock file. | |
| /// | |
| /// The iterable [additionalConstraints] specifies extra constraints the | |
| /// version solver must satisfy. When omitted, no extra constraints are | |
| /// applied. | |
| /// | |
| /// Shows a report of the changes made relative to the previous lockfile. If | |
| /// this is an upgrade or downgrade, all transitive dependencies are shown in | |
| /// the report. Otherwise, only dependencies that were changed are shown. If | |
| /// [dryRun] is `true`, no physical changes are made. | |
| /// | |
| /// [reportMode] specifies the level of reporting output on success. | |
| /// | |
| /// Updates [lockFile] and [packageGraph] accordingly. | |
| /// | |
| /// If [enforceLockfile] is true no changes to the current lockfile are | |
| /// allowed. Instead the existing lockfile is loaded, verified against | |
| /// pubspec.yaml and all dependencies downloaded. | |
| Future<void> acquireDependencies( | |
| SolveType type, { | |
| Iterable<String> unlock = const [], | |
| Iterable<ConstraintAndCause>? additionalConstraints, | |
| bool dryRun = false, | |
| SolveReportMode reportMode = SolveReportMode.full, | |
| bool enforceLockfile = false, | |
| }) async { | |
| workspaceRoot; // This will throw early if pubspec.yaml could not be found. | |
| if (_summaryOnlyEnvironment && reportMode == SolveReportMode.full) { | |
| reportMode = SolveReportMode.summaryOnly; | |
| } | |
| final suffix = | |
| workspaceRoot.dir == '.' | |
| ? '' | |
| : ' in `${workspaceRoot.presentationDir}`'; | |
| if (enforceLockfile && !fileExists(lockFilePath)) { | |
| throw ApplicationException(''' | |
| Retrieving dependencies failed$suffix. | |
| Cannot do `--enforce-lockfile` without an existing `pubspec.lock`. | |
| Try running `$topLevelProgram pub get` to create `$lockFilePath`.'''); | |
| } | |
| SolveResult result; | |
| try { | |
| result = await log.progress('Resolving dependencies$suffix', () async { | |
| // TODO(https://github.com/dart-lang/pub/issues/4127): Check this for | |
| // all workspace pubspecs. | |
| _checkSdkConstraint(workspaceRoot.pubspecPath, workspaceRoot.pubspec); | |
| return resolveVersions( | |
| type, | |
| cache, | |
| workspaceRoot, | |
| lockFile: lockFile, | |
| unlock: unlock, | |
| additionalConstraints: additionalConstraints, | |
| ); | |
| }, transient: reportMode != SolveReportMode.full); | |
| } on SolveFailure catch (e) { | |
| throw SolveFailure( | |
| e.incompatibility, | |
| suggestions: await suggestResolutionAlternatives( | |
| this, | |
| type, | |
| e.incompatibility, | |
| unlock, | |
| cache, | |
| ), | |
| ); | |
| } | |
| result.validatePackageExperiments(); | |
| // We have to download files also with --dry-run to ensure we know the | |
| // archive hashes for downloaded files. | |
| final newLockFile = await result.downloadCachedPackages( | |
| cache, | |
| transient: reportMode != SolveReportMode.full, | |
| ); | |
| final report = SolveReport( | |
| type, | |
| workspaceRoot.presentationDir, | |
| workspaceRoot.pubspec, | |
| workspaceRoot.allOverridesInWorkspace, | |
| lockFile, | |
| newLockFile, | |
| result.availableVersions, | |
| result.experimentUsers, | |
| cache, | |
| dryRun: dryRun, | |
| enforceLockfile: enforceLockfile, | |
| reportMode: reportMode, | |
| expiredExperimentUsers: result.expiredExperimentUsers, | |
| resolvedExperiments: result.resolvedExperiments, | |
| ); | |
| await report.show(summary: true); | |
| if (enforceLockfile && !lockFile.samePackageIds(newLockFile)) { | |
| dataError(''' | |
| Unable to satisfy `${workspaceRoot.pubspecPath}` using `$lockFilePath`$suffix. | |
| To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without | |
| `--enforce-lockfile`.'''); | |
| } | |
| _lockFile = newLockFile; | |
| late final packageGraph = PackageGraph.fromSolveResult(this, result); | |
| if (!(dryRun || enforceLockfile)) { | |
| newLockFile.writeToFile( | |
| lockFilePath, | |
| cache, | |
| dependencies: _resolutionFileDependencies(), | |
| ); | |
| } | |
| if (!dryRun) { | |
| _removeStrayLockAndConfigFiles(); | |
| /// Build a package graph from the version solver results so we don't | |
| /// have to reload and reparse all the pubspecs. | |
| _packageGraph = Future.value(packageGraph); | |
| await writePackageConfigFiles(); | |
| } | |
| } | |
| /// All executables that should be snapshotted from this entrypoint. | |
| /// | |
| /// Only globally activated cached packages precompile executables. | |
| Future<List<Executable>> get _builtExecutables async { | |
| assert(isCachedGlobal); | |
| final graph = await packageGraph; | |
| final r = | |
| workspaceRoot.immediateDependencies.keys.expand((packageName) { | |
| final package = graph.packages[packageName]!; | |
| return package.executablePaths.map( | |
| (path) => Executable(packageName, path), | |
| ); | |
| }).toList(); | |
| return r; | |
| } | |
| /// Precompiles all [_builtExecutables]. | |
| Future<void> precompileExecutables() async { | |
| assert(isCachedGlobal); | |
| final executables = await _builtExecutables; | |
| if (executables.isEmpty) return; | |
| await log.progress('Building package executables', () async { | |
| /// Global snapshots might linger in the cache if we don't remove old | |
| /// snapshots when it is re-activated. | |
| cleanDir(_snapshotPath); | |
| // Don't do more than `platform.numberOfProcessors - 1` compilations | |
| // concurrently. Though at least one. | |
| final pool = Pool(max(platform.numberOfProcessors - 1, 1)); | |
| return waitAndPrintErrors( | |
| executables.map((executable) async { | |
| await pool.withResource(() async { | |
| return _precompileExecutable(executable); | |
| }); | |
| }), | |
| ); | |
| }); | |
| } | |
| /// Precompiles [executable] to a snapshot. | |
| Future<void> precompileExecutable(Executable executable) async { | |
| assert(isCachedGlobal); | |
| await log.progress('Building package executable', () async { | |
| ensureDir(p.dirname(pathOfGlobalSnapshot(executable))); | |
| return await _precompileExecutable(executable); | |
| }, transient: true); | |
| } | |
| Future<void> _precompileExecutable(Executable executable) async { | |
| assert(isCachedGlobal); | |
| final package = executable.package; | |
| await dart.precompile( | |
| executablePath: executable.resolve(packageConfig, packageConfigPath), | |
| outputPath: pathOfGlobalSnapshot(executable), | |
| packageConfigPath: packageConfigPath, | |
| name: '$package:${p.basenameWithoutExtension(executable.relativePath)}', | |
| ); | |
| cache.maintainCache(); | |
| } | |
| /// The location of the snapshot of the dart program at [executable] | |
| /// will be stored here. | |
| /// | |
| /// We use the sdk version to make sure we don't run snapshots from a | |
| /// different sdk. | |
| String pathOfGlobalSnapshot(Executable executable) { | |
| assert(isCachedGlobal); | |
| return executable.pathOfGlobalSnapshot(workspaceRoot.dir); | |
| } | |
| /// Returns the nearest enclosing directory of [dir] that contains a | |
| /// `pubspec.yaml`, or [dir] if none is found. | |
| static String _rootPackageDir(String dir) { | |
| return parentDirs(dir).firstWhereOrNull( | |
| (parent) => tryStatFile(p.join(parent, 'pubspec.yaml')) != null, | |
| ) ?? | |
| dir; | |
| } | |
| /// The [PackageConfig] object representing `.dart_tool/package_config.json` | |
| /// along with the dir where it resides, if it and `pubspec.lock` exist and | |
| /// are up to date with respect to pubspec.yaml and its dependencies. Or | |
| /// `null` if it is outdated. | |
| /// | |
| /// Always returns `null` if `.dart_tool/package_config.json` was generated | |
| /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a | |
| /// different Dart or Flutter SDK version. | |
| /// | |
| /// Otherwise first the `modified` timestamps are compared, and if | |
| /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is | |
| /// newer than all pubspec.yamls of all packages in | |
| /// `.dart_tool/package_config.json` we short-circuit and return true. | |
| /// | |
| /// If any of the timestamps are out of order, the resolution in | |
| /// pubspec.lock is validated against constraints of all pubspec.yamls, and | |
| /// the packages of `.dart_tool/package_config.json` is validated against | |
| /// pubspec.lock. We do this extra round of checking to accommodate for cases | |
| /// where version control or other processes mess up the timestamp order. | |
| /// | |
| /// If the resolution is still valid, the timestamps are updated (unless | |
| /// [updateOutOfDateTimestamps] is false) and this returns the package | |
| /// configuration and the root dir. Otherwise this returns `null`. | |
| /// | |
| /// This check is on the fast-path of `dart run` and should do as little | |
| /// work as possible. Specifically we avoid parsing any yaml when the | |
| /// timestamps are in the right order. | |
| /// | |
| /// `.dart_tool/package_config.json` is read and parsed. In the case of `dart | |
| /// run` this is acceptable: we speculate that it brings it to the file | |
| /// system cache and the dart VM is going to read the file anyways. | |
| /// | |
| /// Note this procedure will give false positives if the timestamps are | |
| /// artificially brought in the "right" order. (eg. by manually running | |
| /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is | |
| /// hard to avoid, but also unlikely to happen by accident because | |
| /// `.dart_tool/package_config.json` is not checked into version control. | |
| static (PackageConfig, String)? isResolutionUpToDate( | |
| String dir, | |
| SystemCache cache, { | |
| bool updateOutOfDateTimestamps = true, | |
| }) { | |
| late final wasRelative = p.isRelative(dir); | |
| String relativeIfNeeded(String path) => | |
| wasRelative ? p.relative(path) : path; | |
| late final rootPackageDir = _rootPackageDir(dir); | |
| late final root = Package.load( | |
| rootPackageDir, | |
| loadPubspec: Pubspec.loadRootWithSources(cache.sources), | |
| ); | |
| late final Package workspaceRoot; | |
| /// Whether the lockfile is out of date with respect to the dependencies' | |
| /// pubspecs. | |
| /// | |
| /// If any mutable pubspec contains dependencies that are not in the | |
| /// lockfile or that don't match what's in there, this will return `false`. | |
| bool isLockFileUpToDate( | |
| LockFile lockFile, | |
| Package root, { | |
| required String lockFilePath, | |
| }) { | |
| /// Returns whether the locked version of [dep] matches the dependency. | |
| bool isDependencyUpToDate(PackageRange dep) { | |
| if (dep.name == root.name) return true; | |
| // Workspace packages are local source packages and are never listed in | |
| // the `packages` section of `pubspec.lock`. They are always considered | |
| // up-to-date here. | |
| if (workspaceRoot.transitiveWorkspace.any((p) => p.name == dep.name)) { | |
| return true; | |
| } | |
| final locked = lockFile.packages[dep.name]; | |
| return locked != null && dep.allows(locked); | |
| } | |
| for (final MapEntry(key: sdkName, value: constraint) | |
| in lockFile.sdkConstraints.entries) { | |
| final sdk = sdks[sdkName]; | |
| if (sdk == null) { | |
| log.fine('Unknown sdk $sdkName in `$lockFilePath`'); | |
| return false; | |
| } | |
| if (!sdk.isAvailable) { | |
| log.fine('sdk: ${sdk.name} not available'); | |
| return false; | |
| } | |
| final sdkVersion = sdk.version; | |
| if (sdkVersion != null) { | |
| if (!constraint.effectiveConstraint.allows(sdkVersion)) { | |
| log.fine( | |
| '`$lockFilePath` requires $sdkName $constraint. ' | |
| 'Current version is $sdkVersion', | |
| ); | |
| return false; | |
| } | |
| } | |
| } | |
| for (final workspacePackage in workspaceRoot.transitiveWorkspace) { | |
| if (!workspacePackage.immediateDependencies.values.every( | |
| isDependencyUpToDate, | |
| )) { | |
| final pubspecPath = p.normalize( | |
| p.join(workspacePackage.dir, 'pubspec.yaml'), | |
| ); | |
| log.fine( | |
| 'The $pubspecPath file has changed since the $lockFilePath file ' | |
| 'was generated.', | |
| ); | |
| return false; | |
| } | |
| } | |
| // Check that uncached dependencies' pubspecs are also still satisfied, | |
| // since they're mutable and may have changed since the last get. | |
| for (var id in lockFile.packages.values) { | |
| final source = id.source; | |
| if (source is CachedSource) continue; | |
| try { | |
| if (cache | |
| .load(id) | |
| .dependencies | |
| .values | |
| .every( | |
| (dep) => | |
| workspaceRoot.allOverridesInWorkspace.containsKey( | |
| dep.name, | |
| ) || | |
| isDependencyUpToDate(dep), | |
| )) { | |
| continue; | |
| } | |
| } on FileException { | |
| // If we can't load the pubspec, the user needs to re-run "pub get". | |
| } | |
| final relativePubspecPath = p.join( | |
| cache.getDirectory(id, relativeFrom: '.'), | |
| 'pubspec.yaml', | |
| ); | |
| log.fine( | |
| '$relativePubspecPath has ' | |
| 'changed since the $lockFilePath file was generated.', | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /// Whether or not the `.dart_tool/package_config.json` file is | |
| /// out of date with respect to the lockfile. | |
| bool isPackageConfigUpToDate( | |
| PackageConfig packageConfig, | |
| LockFile lockFile, | |
| Package root, { | |
| required String packageConfigPath, | |
| required String lockFilePath, | |
| }) { | |
| /// Determines if [lockFile] agrees with the given [packagePathsMapping]. | |
| /// | |
| /// The [packagePathsMapping] is a mapping from package names to paths | |
| /// where the packages are located. (The library is located under `lib/` | |
| /// relative to the path given). | |
| bool isPackagePathsMappingUpToDateWithLockfile( | |
| Map<String, String> packagePathsMapping, { | |
| required String lockFilePath, | |
| required String packageConfigPath, | |
| }) { | |
| // Check that [packagePathsMapping] does not contain more packages than | |
| // what is required. This could lead to import statements working, when | |
| // they are not supposed to work. | |
| final hasExtraMappings = | |
| !packagePathsMapping.keys.every((packageName) { | |
| return workspaceRoot.transitiveWorkspace.any( | |
| (p) => p.name == packageName, | |
| ) || | |
| lockFile.packages.containsKey(packageName); | |
| }); | |
| if (hasExtraMappings) { | |
| return false; | |
| } | |
| // Check that all packages in the workspace are reflected in the | |
| // [packagePathsMapping]. | |
| for (final workspacePackage in workspaceRoot.transitiveWorkspace) { | |
| if (!packagePathsMapping.containsKey(workspacePackage.name)) { | |
| return false; | |
| } | |
| } | |
| // Check that all packages in the [lockFile] are reflected in the | |
| // [packagePathsMapping]. | |
| return lockFile.packages.values.every((lockFileId) { | |
| // It's very unlikely that the lockfile is invalid here, but it's not | |
| // impossible—for example, the user may have a very old application | |
| // package with a checked-in lockfile that's newer than the pubspec, | |
| // but that contains SDK dependencies. | |
| if (lockFileId.source is UnknownSource) return false; | |
| final packagePath = packagePathsMapping[lockFileId.name]; | |
| if (packagePath == null) { | |
| return false; | |
| } | |
| final source = lockFileId.source; | |
| final lockFilePackagePath = workspaceRoot.path( | |
| cache.getDirectory(lockFileId, relativeFrom: workspaceRoot.dir), | |
| ); | |
| // Make sure that the packagePath agrees with the lock file about the | |
| // path to the package. | |
| if (p.normalize(packagePath) != p.normalize(lockFilePackagePath)) { | |
| return false; | |
| } | |
| // For cached sources, make sure the directory exists and looks like a | |
| // package. This is also done by [_arePackagesAvailable] but that may | |
| // not be run if the lockfile is newer than the pubspec. | |
| if (source is CachedSource && !dirExists(lockFilePackagePath) || | |
| !fileExists(p.join(lockFilePackagePath, 'pubspec.yaml'))) { | |
| return false; | |
| } | |
| return true; | |
| }); | |
| } | |
| final packagePathsMapping = <String, String>{}; | |
| final packagesToCheck = packageConfig.packages; | |
| for (final pkg in packagesToCheck) { | |
| // Pub always makes a packageUri of lib/ | |
| if (pkg.packageUri == null || pkg.packageUri.toString() != 'lib/') { | |
| log.fine( | |
| 'The "$packageConfigPath" file ' | |
| 'is not recognized by this pub version.', | |
| ); | |
| return false; | |
| } | |
| packagePathsMapping[pkg.name] = workspaceRoot.path( | |
| '.dart_tool', | |
| p.fromUri(pkg.rootUri), | |
| ); | |
| } | |
| if (!isPackagePathsMappingUpToDateWithLockfile( | |
| packagePathsMapping, | |
| packageConfigPath: packageConfigPath, | |
| lockFilePath: lockFilePath, | |
| )) { | |
| log.fine( | |
| 'The $lockFilePath file has changed since the ' | |
| '$packageConfigPath file ' | |
| 'was generated, please run "$topLevelProgram pub get" again.', | |
| ); | |
| return false; | |
| } | |
| // Check if language version specified in the `package_config.json` is | |
| // correct. This is important for path dependencies as these can mutate. | |
| for (final pkg in packageConfig.packages) { | |
| final workspacePkg = workspaceRoot.transitiveWorkspace.firstWhereOrNull( | |
| (p) => p.name == pkg.name, | |
| ); | |
| if (workspacePkg != null) { | |
| if (pkg.languageVersion != workspacePkg.pubspec.languageVersion) { | |
| log.fine( | |
| '${workspacePkg.pubspecPath} has ' | |
| 'changed since the $lockFilePath file was generated.', | |
| ); | |
| return false; | |
| } | |
| continue; | |
| } | |
| final id = lockFile.packages[pkg.name]; | |
| if (id == null) { | |
| assert( | |
| false, | |
| 'unnecessary package_config.json entries should be forbidden by ' | |
| '_isPackagePathsMappingUpToDateWithLockfile', | |
| ); | |
| continue; | |
| } | |
| // If a package is cached, then it's universally immutable and we need | |
| // not check if the language version is correct. | |
| final source = id.source; | |
| if (source is CachedSource) { | |
| continue; | |
| } | |
| try { | |
| // Load `pubspec.yaml` and extract language version to compare with | |
| // the language version from `package_config.json`. | |
| final languageVersion = cache.load(id).pubspec.languageVersion; | |
| if (pkg.languageVersion != languageVersion) { | |
| final relativePubspecPath = p.join( | |
| cache.getDirectory(id, relativeFrom: '.'), | |
| 'pubspec.yaml', | |
| ); | |
| log.fine( | |
| '$relativePubspecPath has ' | |
| 'changed since the $lockFilePath file was generated.', | |
| ); | |
| return false; | |
| } | |
| } on FileException { | |
| log.fine( | |
| 'Failed to read pubspec.yaml for "${pkg.name}", perhaps the ' | |
| 'entry is missing.', | |
| ); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| FileStat? packageConfigStat; | |
| late final String packageConfigPath; | |
| late final String rootDir; | |
| for (final parent in parentDirs(dir)) { | |
| final potentialPackageConfigPath = p.normalize( | |
| p.join(parent, '.dart_tool', 'package_config.json'), | |
| ); | |
| packageConfigStat = tryStatFile(potentialPackageConfigPath); | |
| if (packageConfigStat != null) { | |
| packageConfigPath = potentialPackageConfigPath; | |
| rootDir = parent; | |
| break; | |
| } | |
| final potentialPubspecPath = p.join(parent, 'pubspec.yaml'); | |
| if (tryStatFile(potentialPubspecPath) == null) { | |
| // No package at [parent] continue to next dir. | |
| continue; | |
| } | |
| final potentialWorkspaceRefPath = p.normalize( | |
| p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'), | |
| ); | |
| final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath); | |
| if (workspaceRefText == null) { | |
| log.fine( | |
| '`$potentialPubspecPath` exists without corresponding ' | |
| '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.', | |
| ); | |
| return null; | |
| } else { | |
| try { | |
| if (jsonDecode(workspaceRefText) case { | |
| 'workspaceRoot': final String path, | |
| }) { | |
| final potentialPackageConfigPath2 = relativeIfNeeded( | |
| p.normalize( | |
| p.absolute( | |
| p.join( | |
| p.dirname(potentialWorkspaceRefPath), | |
| path, | |
| '.dart_tool', | |
| 'package_config.json', | |
| ), | |
| ), | |
| ), | |
| ); | |
| packageConfigStat = tryStatFile(potentialPackageConfigPath2); | |
| if (packageConfigStat == null) { | |
| log.fine( | |
| '`$potentialWorkspaceRefPath` points to non-existing ' | |
| '`$potentialPackageConfigPath2`', | |
| ); | |
| return null; | |
| } else { | |
| packageConfigPath = potentialPackageConfigPath2; | |
| rootDir = relativeIfNeeded( | |
| p.normalize( | |
| p.absolute( | |
| p.join(p.dirname(potentialWorkspaceRefPath), path), | |
| ), | |
| ), | |
| ); | |
| break; | |
| } | |
| } else { | |
| log.fine( | |
| '`$potentialWorkspaceRefPath` ' | |
| 'is missing "workspaceRoot" property', | |
| ); | |
| return null; | |
| } | |
| } on FormatException catch (e) { | |
| log.fine('`$potentialWorkspaceRefPath` not valid json: $e.'); | |
| return null; | |
| } | |
| } | |
| } | |
| if (packageConfigStat == null) { | |
| log.fine( | |
| 'Found no .dart_tool/package_config.json - no existing resolution.', | |
| ); | |
| return null; | |
| } | |
| final packageConfig = _loadPackageConfig(packageConfigPath); | |
| if (p.isWithin(cache.rootDir, packageConfigPath)) { | |
| // We always consider a global package (inside the cache) up-to-date. | |
| return (packageConfig, rootDir); | |
| } | |
| workspaceRoot = | |
| rootDir == rootPackageDir | |
| ? root | |
| : Package.load( | |
| rootDir, | |
| loadPubspec: Pubspec.loadRootWithSources(cache.sources), | |
| ); | |
| final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock')); | |
| /// Whether or not the `.dart_tool/package_config.json` file was | |
| /// generated by a different sdk down to changes in minor versions. | |
| bool isPackageConfigGeneratedBySameDartSdk() { | |
| final generatorVersion = packageConfig.generatorVersion; | |
| if (generatorVersion == null || | |
| generatorVersion.major != sdk.version.major || | |
| generatorVersion.minor != sdk.version.minor) { | |
| log.fine('The Dart SDK was updated since last package resolution.'); | |
| return false; | |
| } | |
| return true; | |
| } | |
| final flutter = FlutterSdk(); | |
| // If Flutter has moved since last invocation, we want to have new | |
| // sdk-packages, and therefore do a new resolution. | |
| // | |
| // This also counts if Flutter was introduced or removed. | |
| final flutterRoot = | |
| flutter.rootDirectory == null | |
| ? null | |
| : p.toUri(p.absolute(flutter.rootDirectory!)).toString(); | |
| if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) { | |
| log.fine('Flutter has moved since last invocation.'); | |
| return null; | |
| } | |
| if (packageConfig.additionalProperties['flutterVersion'] != | |
| (flutter.isAvailable ? flutter.version.toString() : null)) { | |
| log.fine('Flutter has updated since last invocation.'); | |
| return null; | |
| } | |
| // If the pub cache was moved we should have a new resolution. | |
| final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString(); | |
| if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) { | |
| final previousPubCachePath = | |
| packageConfig.additionalProperties['pubCache']; | |
| log.fine( | |
| 'The pub cache has moved from $previousPubCachePath to $rootCacheUrl ' | |
| 'since last invocation.', | |
| ); | |
| return null; | |
| } | |
| // If the Dart sdk was updated we want a new resolution. | |
| if (!isPackageConfigGeneratedBySameDartSdk()) { | |
| return null; | |
| } | |
| final lockFileStat = tryStatFile(lockFilePath); | |
| if (lockFileStat == null) { | |
| log.fine('No $lockFilePath file found.'); | |
| return null; | |
| } | |
| final lockFileModified = lockFileStat.modified; | |
| var lockfileNewerThanPubspecs = true; | |
| // Whether any pubspec is strictly newer than the lockfile. | |
| // We only touch the lockfile to make it newer if this is true, avoiding | |
| // touching it on equal timestamps to prevent cascading invalidation of | |
| // .dart_tool/package_config.json and downstream developer tools. | |
| var pubspecStrictlyNewer = false; | |
| // Check that all packages in packageConfig exist and their pubspecs have | |
| // not been updated since the lockfile was written. | |
| for (var package in packageConfig.packages) { | |
| final pubspecPath = p.normalize( | |
| p.join( | |
| rootDir, | |
| '.dart_tool', | |
| package.rootUri | |
| // Important to use `toFilePath()` here rather than `path`, as | |
| // it handles Url-decoding. | |
| .toFilePath(), | |
| 'pubspec.yaml', | |
| ), | |
| ); | |
| if (p.isWithin(cache.rootDir, pubspecPath)) { | |
| continue; | |
| } | |
| final pubspecStat = tryStatFile(pubspecPath); | |
| if (pubspecStat == null) { | |
| log.fine('Could not find `$pubspecPath`'); | |
| // A dependency is missing - do a full new resolution. | |
| return null; | |
| } | |
| if (!lockFileModified.isAfter(pubspecStat.modified)) { | |
| log.fine('`$pubspecPath` is newer than `$lockFilePath`'); | |
| lockfileNewerThanPubspecs = false; | |
| if (pubspecStat.modified.isAfter(lockFileModified)) { | |
| pubspecStrictlyNewer = true; | |
| break; | |
| } | |
| } | |
| final pubspecOverridesPath = p.join( | |
| package.rootUri.path, | |
| 'pubspec_overrides.yaml', | |
| ); | |
| final pubspecOverridesStat = tryStatFile(pubspecOverridesPath); | |
| if (pubspecOverridesStat != null) { | |
| // This will wrongly require you to reresolve if a | |
| // `pubspec_overrides.yaml` in a path-dependency is updated. That | |
| // seems acceptable. | |
| if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) { | |
| log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`'); | |
| lockfileNewerThanPubspecs = false; | |
| if (pubspecOverridesStat.modified.isAfter(lockFileModified)) { | |
| pubspecStrictlyNewer = true; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (!updateOutOfDateTimestamps && !lockfileNewerThanPubspecs) { | |
| log.fine( | |
| 'Timestamps are out of order (updateOutOfDateTimestamps: false)', | |
| ); | |
| return null; | |
| } | |
| var touchedLockFile = false; | |
| late final lockFile = _loadLockFile(lockFilePath, cache); | |
| if (!lockfileNewerThanPubspecs) { | |
| if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) { | |
| if (pubspecStrictlyNewer) { | |
| touch(lockFilePath); | |
| touchedLockFile = true; | |
| } | |
| } else { | |
| return null; | |
| } | |
| } | |
| if (!updateOutOfDateTimestamps && | |
| packageConfigStat.modified.isBefore(lockFileModified)) { | |
| log.fine( | |
| 'Timestamps are out of order (updateOutOfDateTimestamps: false)', | |
| ); | |
| return null; | |
| } | |
| if (touchedLockFile || | |
| !lockfileNewerThanPubspecs || | |
| packageConfigStat.modified.isBefore(lockFileModified)) { | |
| log.fine('`$lockFilePath` is newer than `$packageConfigPath`'); | |
| if (isPackageConfigUpToDate( | |
| packageConfig, | |
| lockFile, | |
| root, | |
| packageConfigPath: packageConfigPath, | |
| lockFilePath: lockFilePath, | |
| )) { | |
| if (touchedLockFile || | |
| lockFileModified.isAfter(packageConfigStat.modified)) { | |
| touch(packageConfigPath); | |
| } | |
| } else { | |
| return null; | |
| } | |
| } | |
| return (packageConfig, rootDir); | |
| } | |
| /// Does a fast-pass check to see if the resolution is up-to-date. If not, run | |
| /// a resolution with `pub get` semantics. | |
| /// | |
| /// [reportMode] specifies the level of reporting output on success. Defaults | |
| /// to [SolveReportMode.none] (no output). | |
| /// | |
| /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no | |
| /// output if no terminal is attached. | |
| /// | |
| /// When succesfull returns the found/created `PackageConfig` and the | |
| /// directory containing it. | |
| static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate( | |
| String dir, { | |
| required SystemCache cache, | |
| SolveReportMode reportMode = SolveReportMode.none, | |
| bool onlyOutputWhenTerminal = true, | |
| }) async { | |
| late final wasRelative = p.isRelative(dir); | |
| String relativeIfNeeded(String path) => | |
| wasRelative ? p.relative(path) : path; | |
| if (isResolutionUpToDate(dir, cache) case ( | |
| final PackageConfig packageConfig, | |
| final String rootDir, | |
| )) { | |
| log.fine('Package Config up to date.'); | |
| return (packageConfig: packageConfig, rootDir: rootDir); | |
| } | |
| final rootPackageDir = _rootPackageDir(dir); | |
| final entrypoint = Entrypoint( | |
| rootPackageDir, | |
| cache, | |
| // [ensureUpToDate] is also used for entries in 'global_packages/' | |
| checkInCache: false, | |
| ); | |
| if (onlyOutputWhenTerminal) { | |
| await log.errorsOnlyUnlessTerminal(() async { | |
| await entrypoint.acquireDependencies( | |
| SolveType.get, | |
| reportMode: reportMode, | |
| ); | |
| }); | |
| } else { | |
| await entrypoint.acquireDependencies( | |
| SolveType.get, | |
| reportMode: reportMode, | |
| ); | |
| } | |
| return ( | |
| packageConfig: entrypoint.packageConfig, | |
| rootDir: relativeIfNeeded( | |
| p.normalize(p.absolute(entrypoint.workspaceRoot.dir)), | |
| ), | |
| ); | |
| } | |
| /// We require an SDK constraint lower-bound as of Dart 2.12.0 | |
| /// | |
| /// We don't allow unknown sdks. | |
| void _checkSdkConstraint(String pubspecPath, Pubspec pubspec) { | |
| final dartSdkConstraint = pubspec.dartSdkConstraint.effectiveConstraint; | |
| // Suggest an sdk constraint giving the same language version as the | |
| // current sdk. | |
| var suggestedConstraint = VersionConstraint.compatibleWith( | |
| Version(sdk.version.major, sdk.version.minor, 0), | |
| ); | |
| // But if somehow that doesn't work, we fallback to safe sanity, mostly | |
| // important for tests, or if we jump to 3.x without patching this code. | |
| if (!suggestedConstraint.allows(sdk.version)) { | |
| suggestedConstraint = VersionRange( | |
| min: sdk.version, | |
| max: sdk.version.nextBreaking, | |
| includeMin: true, | |
| ); | |
| } | |
| if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) { | |
| throw DataException(''' | |
| $pubspecPath has no lower-bound SDK constraint. | |
| You should edit $pubspecPath to contain an SDK constraint: | |
| environment: | |
| sdk: '${suggestedConstraint.asCompatibleWithIfPossible()}' | |
| See https://dart.dev/go/sdk-constraint | |
| '''); | |
| } | |
| if (!LanguageVersion.fromSdkConstraint( | |
| dartSdkConstraint, | |
| ).supportsNullSafety) { | |
| throw DataException(''' | |
| The lower bound of "sdk: '$dartSdkConstraint'" must be 2.12.0' | |
| or higher to enable null safety. | |
| The current Dart SDK (${sdk.version}) only supports null safety. | |
| For details, see https://dart.dev/null-safety | |
| '''); | |
| } | |
| for (final sdk in pubspec.sdkConstraints.keys) { | |
| if (!sdks.containsKey(sdk)) { | |
| final environment = pubspec.fields.nodes['environment'] as YamlMap; | |
| final keyNode = | |
| environment.nodes.entries | |
| .firstWhere((e) => (e.key as YamlNode).value == sdk) | |
| .key | |
| as YamlNode; | |
| throw SourceSpanApplicationException(''' | |
| $pubspecPath refers to an unknown sdk '$sdk'. | |
| Did you mean to add it as a dependency? | |
| Either remove the constraint, or upgrade to a version of pub that supports the | |
| given sdk. | |
| See https://dart.dev/go/sdk-constraint | |
| ''', keyNode.span); | |
| } | |
| } | |
| } | |
| /// Setting the `PUB_SUMMARY_ONLY` environment variable to anything but '0' | |
| /// will result in [acquireDependencies] to only print a summary of the | |
| /// results. | |
| bool get _summaryOnlyEnvironment => | |
| (platform.environment['PUB_SUMMARY_ONLY'] ?? '0') != '0'; | |
| /// Remove any `pubspec.lock` or `.dart_tool/package_config.json` files in | |
| /// workspace packages that are not the root package. | |
| /// | |
| /// Also remove from directories between the workspace package and the | |
| /// workspace root, to prevent stray package configs from shadowing the shared | |
| /// workspace package config. | |
| /// | |
| /// This is to avoid surprises if a package is turned into a workspace member | |
| /// but still has an old package config or lockfile. | |
| void _removeStrayLockAndConfigFiles() { | |
| final visited = <String>{ | |
| // By adding this to visited we will never go above the workspaceRoot.dir. | |
| p.canonicalize(workspaceRoot.dir), | |
| }; | |
| var deletedAny = false; | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| if (package.pubspec.resolution == Resolution.workspace) { | |
| for (final dir in parentDirs(package.dir)) { | |
| if (!visited.add(p.canonicalize(dir))) { | |
| // No reason to delete from the same directory twice. | |
| break; | |
| } | |
| void deleteIfPresent(String path, String type) { | |
| if (fileExists(path)) { | |
| log.warning('Deleting old $type: `$path`.'); | |
| deleteEntry(path); | |
| deletedAny = true; | |
| } | |
| } | |
| deleteIfPresent(p.join(dir, 'pubspec.lock'), 'lock-file'); | |
| deleteIfPresent( | |
| p.join(dir, '.dart_tool', 'package_config.json'), | |
| 'package config', | |
| ); | |
| } | |
| } | |
| } | |
| if (deletedAny) { | |
| log.warning( | |
| 'See https://dart.dev/go/workspaces-stray-files for details.', | |
| ); | |
| } | |
| } | |
| /// Returns a list of changes to constraints of workspace pubspecs updated to | |
| /// have their lower bound match the version in [packageVersions] (or | |
| /// `this.lockFile`). | |
| /// | |
| /// The return value for each workspace package is a mapping from the original | |
| /// package range to the updated. | |
| /// | |
| /// If packages to update where given in [packagesToUpgrade], only those are | |
| /// tightened. Otherwise all packages are tightened. | |
| /// | |
| /// If a dependency has already been updated in [existingChanges], the update | |
| /// will apply on top of that change (eg. preserving the new upper bound). | |
| Map<Package, Map<PackageRange, PackageRange>> tighten({ | |
| List<String> packagesToUpgrade = const [], | |
| Map<Package, Map<PackageRange, PackageRange>> existingChanges = const {}, | |
| List<PackageId>? packageVersions, | |
| }) { | |
| final result = {...existingChanges}; | |
| final toTighten = <(Package, PackageRange)>[]; | |
| // Keep track of the versions of workspace packages - these are not included | |
| // in the lockfile. | |
| final workspaceVersions = <String, Version>{}; | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| workspaceVersions[package.name] = package.version; | |
| if (packagesToUpgrade.isEmpty) { | |
| for (final range in [ | |
| ...package.dependencies.values, | |
| ...package.devDependencies.values, | |
| ]) { | |
| toTighten.add((package, range)); | |
| } | |
| } else { | |
| for (final packageToUpgrade in packagesToUpgrade) { | |
| final range = | |
| package.dependencies[packageToUpgrade] ?? | |
| package.devDependencies[packageToUpgrade]; | |
| if (range != null) { | |
| toTighten.add((package, range)); | |
| } | |
| } | |
| } | |
| } | |
| for (final (package, range) in toTighten) { | |
| final changesForPackage = result[package] ??= {}; | |
| final constraint = (changesForPackage[range] ?? range).constraint; | |
| final resolvedVersion = | |
| (packageVersions?.firstWhere((p) => p.name == range.name) ?? | |
| lockFile.packages[range.name]) | |
| ?.version ?? | |
| workspaceVersions[range.name]!; | |
| if (range.source is HostedSource && constraint.isAny) { | |
| changesForPackage[range] = range.toRef().withConstraint( | |
| VersionConstraint.compatibleWith(resolvedVersion), | |
| ); | |
| } else if (constraint is VersionRange) { | |
| final min = constraint.min; | |
| if (min != null && min < resolvedVersion) { | |
| changesForPackage[range] = range.toRef().withConstraint( | |
| VersionRange( | |
| min: resolvedVersion, | |
| max: constraint.max, | |
| includeMin: true, | |
| includeMax: constraint.includeMax, | |
| ).asCompatibleWithIfPossible(), | |
| ); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| /// Unless [dryRun], loads `pubspec.yaml` of each [Package] in [changeSet] and | |
| /// applies the changes to its (dev)-dependencies using yaml_edit to preserve | |
| /// textual structure. | |
| /// | |
| /// Outputs a summary of changes done or would have been done if not [dryRun]. | |
| void applyChanges(ChangeSet changeSet, bool dryRun) { | |
| if (!dryRun) { | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| final changesForPackage = changeSet[package]; | |
| if (changesForPackage == null || changesForPackage.isEmpty) { | |
| continue; | |
| } | |
| final yamlEditor = YamlEditor(readTextFile(package.pubspecPath)); | |
| final deps = package.dependencies.keys; | |
| for (final change in changesForPackage.values) { | |
| final section = | |
| deps.contains(change.name) ? 'dependencies' : 'dev_dependencies'; | |
| yamlEditor.update([ | |
| section, | |
| change.name, | |
| ], pubspecDescription(change, cache, package)); | |
| } | |
| writeTextFile(package.pubspecPath, yamlEditor.toString()); | |
| } | |
| } | |
| _outputChangeSummary(changeSet, dryRun: dryRun); | |
| } | |
| /// Outputs a summary of [changeSet]. | |
| void _outputChangeSummary(ChangeSet changeSet, {required bool dryRun}) { | |
| if (workspaceRoot.workspaceChildren.isEmpty) { | |
| final changesToWorkspaceRoot = changeSet[workspaceRoot] ?? {}; | |
| if (changesToWorkspaceRoot.isEmpty) { | |
| final wouldBe = dryRun ? 'would be made to' : 'to'; | |
| log.message('\nNo changes $wouldBe pubspec.yaml!'); | |
| } else { | |
| final changed = dryRun ? 'Would change' : 'Changed'; | |
| final constraints = pluralize( | |
| 'constraint', | |
| changesToWorkspaceRoot.length, | |
| ); | |
| log.message( | |
| '\n$changed ${changesToWorkspaceRoot.length} ' | |
| '$constraints in pubspec.yaml:', | |
| ); | |
| changesToWorkspaceRoot.forEach((from, to) { | |
| log.message(' ${from.name}: ${from.constraint} -> ${to.constraint}'); | |
| }); | |
| } | |
| } else { | |
| if (changeSet.isEmpty) { | |
| final wouldBe = dryRun ? 'would be made to' : 'to'; | |
| log.message('\nNo changes $wouldBe any pubspec.yaml!'); | |
| } | |
| for (final package in workspaceRoot.transitiveWorkspace) { | |
| final changesToPackage = changeSet[package] ?? {}; | |
| if (changesToPackage.isEmpty) continue; | |
| final changed = dryRun ? 'Would change' : 'Changed'; | |
| final constraints = pluralize('constraint', changesToPackage.length); | |
| log.message( | |
| '\n$changed ${changesToPackage.length} ' | |
| '$constraints in ${package.pubspecPath}:', | |
| ); | |
| changesToPackage.forEach((from, to) { | |
| log.message(' ${from.name}: ${from.constraint} -> ${to.constraint}'); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| /// For each package in a workspace, a set of changes to dependencies. | |
| typedef ChangeSet = Map<Package, Map<PackageRange, PackageRange>>; |