| // 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:meta/meta.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pool/pool.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'command_runner.dart'; |
| import 'dart.dart' as dart; |
| import 'exceptions.dart'; |
| import 'executable.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_config.dart' show PackageConfig; |
| import 'package_graph.dart'; |
| import 'package_name.dart'; |
| import 'packages_file.dart' as packages_file; |
| import 'pub_embeddable_command.dart'; |
| import 'pubspec.dart'; |
| import 'sdk.dart'; |
| import 'solver.dart'; |
| import 'source/cached.dart'; |
| import 'source/unknown.dart'; |
| import 'system_cache.dart'; |
| import 'utils.dart'; |
| |
| /// A RegExp to match SDK constraints in a lockfile. |
| final _sdkConstraint = () { |
| // This matches both the old-style constraint: |
| // |
| // ```yaml |
| // sdk: ">=1.2.3 <2.0.0" |
| // ``` |
| // |
| // and the new-style constraint: |
| // |
| // ```yaml |
| // sdks: |
| // dart: ">=1.2.3 <2.0.0" |
| // ``` |
| var sdkNames = sdks.keys.map((name) => ' ' + name).join('|'); |
| return RegExp(r'^(' + sdkNames + r'|sdk): "?([^"]*)"?$', multiLine: true); |
| }(); |
| |
| /// The context surrounding the root package 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 [root] package it is |
| /// associated with and is responsible for managing the "packages" directory |
| /// for it. |
| /// |
| /// That directory contains symlinks to all packages used by an app. These links |
| /// point either to the [SystemCache] or to some other location on the local |
| /// filesystem. |
| /// |
| /// While entrypoints are typically applications, a pure library package may end |
| /// up being used as an entrypoint. 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 root package this entrypoint is associated with. |
| /// |
| /// For a global package, this is the activated package. |
| final Package root; |
| |
| /// For a global package, this is the directory that the package is installed |
| /// in. Non-global packages have null. |
| final String? globalDir; |
| |
| /// 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 => !root.isInMemory && p.isWithin(cache.rootDir, root.dir); |
| |
| /// Whether this is an entrypoint for a globally-activated package. |
| // final bool isGlobal; |
| bool get isGlobal => globalDir != null; |
| |
| /// The lockfile for the entrypoint. |
| /// |
| /// If not provided to the entrypoint, it will be loaded lazily from disk. |
| LockFile get lockFile => _lockFile ??= _loadLockFile(); |
| |
| LockFile _loadLockFile() { |
| if (!fileExists(lockFilePath)) { |
| return _lockFile = LockFile.empty(); |
| } else { |
| return _lockFile = LockFile.load(lockFilePath, cache.sources); |
| } |
| } |
| |
| LockFile? _lockFile; |
| |
| /// The package graph for the application and all of its transitive |
| /// dependencies. |
| /// |
| /// Throws a [DataError] if the `.dart_tool/package_config.json` file isn't |
| /// up-to-date relative to the pubspec and the lockfile. |
| PackageGraph get packageGraph => _packageGraph ??= _createPackageGraph(); |
| |
| PackageGraph _createPackageGraph() { |
| assertUpToDate(); |
| var packages = { |
| for (var id in lockFile.packages.values) id.name: cache.load(id) |
| }; |
| packages[root.name] = root; |
| |
| return PackageGraph(this, lockFile, packages); |
| } |
| |
| PackageGraph? _packageGraph; |
| |
| /// Where the lock file and package configurations are to be found. |
| /// |
| /// Global packages (except those from path source) |
| /// store these in the global cache. |
| String? get _configRoot => isCached ? globalDir : root.dir; |
| |
| /// The path to the entrypoint's ".packages" file. |
| /// |
| /// This file is being slowly deprecated in favor of |
| /// `.dart_tool/package_config.json`. Pub will still create it, but will |
| /// not require it or make use of it within pub. |
| String get packagesFile => p.normalize(p.join(_configRoot!, '.packages')); |
| |
| /// The path to the entrypoint's ".dart_tool/package_config.json" file. |
| String get packageConfigFile => |
| p.normalize(p.join(_configRoot!, '.dart_tool', 'package_config.json')); |
| |
| /// The path to the entrypoint package's pubspec. |
| String get pubspecPath => p.normalize(root.path('pubspec.yaml')); |
| |
| /// Whether the entrypoint package contains a `pubspec_overrides.yaml` file. |
| bool get hasPubspecOverrides => |
| !root.isInMemory && fileExists(pubspecOverridesPath); |
| |
| /// The path to the entrypoint package's pubspec overrides file. |
| String get pubspecOverridesPath => |
| p.normalize(root.path('pubspec_overrides.yaml')); |
| |
| /// The path to the entrypoint package's lockfile. |
| String get lockFilePath => p.normalize(p.join(_configRoot!, 'pubspec.lock')); |
| |
| /// The path to the entrypoint package's `.dart_tool/pub` cache directory. |
| /// |
| /// If the old-style `.pub` directory is being used, this returns that |
| /// instead. |
| /// |
| /// For globally activated packages from path, this is not the same as |
| /// [configRoot], because the snapshots should be stored in the global cache, |
| /// but the configuration is stored at the package itself. |
| String get cachePath { |
| if (isGlobal) { |
| return globalDir!; |
| } else { |
| var newPath = root.path('.dart_tool/pub'); |
| var oldPath = root.path('.pub'); |
| if (!dirExists(newPath) && dirExists(oldPath)) return oldPath; |
| return newPath; |
| } |
| } |
| |
| /// The path to the directory containing dependency executable snapshots. |
| String get _snapshotPath => p.join(cachePath, 'bin'); |
| |
| /// The path to the directory containing previous dill files for incremental |
| /// builds. |
| String get _incrementalDillsPath => p.join(cachePath, 'incremental'); |
| |
| /// Loads the entrypoint from a package at [rootDir]. |
| Entrypoint( |
| String rootDir, |
| this.cache, { |
| bool withPubspecOverrides = true, |
| }) : root = Package.load(null, rootDir, cache.sources, |
| withPubspecOverrides: withPubspecOverrides), |
| globalDir = null; |
| |
| Entrypoint.inMemory(this.root, this.cache, |
| {required LockFile? lockFile, SolveResult? solveResult}) |
| : _lockFile = lockFile, |
| globalDir = null { |
| if (solveResult != null) { |
| _packageGraph = PackageGraph.fromSolveResult(this, solveResult); |
| } |
| } |
| |
| /// Creates an entrypoint given package and lockfile objects. |
| /// If a SolveResult is already created it can be passed as an optimization. |
| Entrypoint.global(this.globalDir, this.root, this._lockFile, this.cache, |
| {SolveResult? solveResult}) { |
| if (solveResult != null) { |
| _packageGraph = PackageGraph.fromSolveResult(this, solveResult); |
| } |
| } |
| |
| /// Gets the [Entrypoint] package for the current working directory. |
| /// |
| /// This will be null if the example folder doesn't have a `pubspec.yaml`. |
| Entrypoint? get example { |
| if (_example != null) return _example; |
| if (!fileExists(root.path('example', 'pubspec.yaml'))) { |
| return null; |
| } |
| return _example = Entrypoint(root.path('example'), cache); |
| } |
| |
| Entrypoint? _example; |
| |
| /// Writes .packages and .dart_tool/package_config.json |
| Future<void> writePackagesFiles({bool generateDotPackages = false}) async { |
| final entrypointName = isGlobal ? null : root.name; |
| if (generateDotPackages) { |
| writeTextFile( |
| packagesFile, |
| lockFile.packagesFile(cache, |
| entrypoint: entrypointName, |
| relativeFrom: isGlobal ? null : root.dir)); |
| } else { |
| tryDeleteEntry(packagesFile); |
| } |
| ensureDir(p.dirname(packageConfigFile)); |
| writeTextFile( |
| packageConfigFile, |
| await lockFile.packageConfigFile(cache, |
| entrypoint: entrypointName, |
| entrypointSdkConstraint: |
| root.pubspec.sdkConstraints[sdk.identifier], |
| relativeFrom: isGlobal ? null : root.dir)); |
| } |
| |
| /// Gets all dependencies of the [root] package. |
| /// |
| /// Performs version resolution according to [SolveType]. |
| /// |
| /// [useLatest], if provided, defines a list of packages that will be |
| /// unlocked and forced to their latest versions. If [upgradeAll] is |
| /// true, the previous lockfile is ignored and all packages are re-resolved |
| /// from scratch. Otherwise, it will attempt to preserve the versions of all |
| /// previously locked packages. |
| /// |
| /// 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. |
| /// |
| /// If [precompile] is `true` (the default), this snapshots dependencies' |
| /// executables. |
| /// |
| /// if [onlyReportSuccessOrFailure] is `true` only success or failure will be shown --- |
| /// in case of failure, a reproduction command is shown. |
| /// |
| /// Updates [lockFile] and [packageRoot] accordingly. |
| Future<void> acquireDependencies( |
| SolveType type, { |
| Iterable<String>? unlock, |
| bool dryRun = false, |
| bool precompile = false, |
| required bool generateDotPackages, |
| required PubAnalytics? analytics, |
| bool onlyReportSuccessOrFailure = false, |
| }) async { |
| if (!onlyReportSuccessOrFailure && hasPubspecOverrides) { |
| log.warning( |
| 'Warning: pubspec.yaml has overrides from $pubspecOverridesPath'); |
| } |
| |
| final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}'; |
| SolveResult result; |
| try { |
| result = await log.progress('Resolving dependencies$suffix', () async { |
| _checkSdkConstraint(root.pubspec); |
| return resolveVersions( |
| type, |
| cache, |
| root, |
| lockFile: lockFile, |
| unlock: unlock ?? [], |
| ); |
| }); |
| } catch (e) { |
| if (onlyReportSuccessOrFailure && (e is ApplicationException)) { |
| final directoryOption = root.isInMemory || root.dir == '.' |
| ? '' |
| : ' --directory ${root.dir}'; |
| throw ApplicationException( |
| 'Resolving dependencies$suffix failed. For details run `$topLevelProgram pub ${type.toString()}$directoryOption`'); |
| } else { |
| rethrow; |
| } |
| } |
| |
| // Log once about all overridden packages. |
| if (warnAboutPreReleaseSdkOverrides) { |
| var overriddenPackages = (result.pubspecs.values |
| .where((pubspec) => pubspec.dartSdkWasOverridden) |
| .map((pubspec) => pubspec.name) |
| .toList() |
| ..sort()) |
| .join(', '); |
| if (overriddenPackages.isNotEmpty) { |
| log.message(log.yellow( |
| 'Overriding the upper bound Dart SDK constraint to <=${sdk.version} ' |
| 'for the following packages:\n\n$overriddenPackages\n\n' |
| 'To disable this you can set the PUB_ALLOW_PRERELEASE_SDK system ' |
| 'environment variable to `false`, or you can silence this message ' |
| 'by setting it to `quiet`.')); |
| } |
| } |
| |
| if (!onlyReportSuccessOrFailure) { |
| await result.showReport(type, cache); |
| } |
| if (!dryRun) { |
| await result.downloadCachedPackages(cache); |
| saveLockFile(result); |
| } |
| if (onlyReportSuccessOrFailure) { |
| log.message('Got dependencies$suffix.'); |
| } else { |
| await result.summarizeChanges(type, cache, dryRun: dryRun); |
| } |
| |
| if (!dryRun) { |
| if (analytics != null) { |
| result.sendAnalytics(analytics); |
| } |
| |
| /// Build a package graph from the version solver results so we don't |
| /// have to reload and reparse all the pubspecs. |
| _packageGraph = PackageGraph.fromSolveResult(this, result); |
| |
| await writePackagesFiles(generateDotPackages: generateDotPackages); |
| |
| try { |
| if (precompile) { |
| await precompileExecutables(); |
| } else { |
| _deleteExecutableSnapshots(changed: result.changedPackages); |
| } |
| } catch (error, stackTrace) { |
| // Just log exceptions here. Since the method is just about acquiring |
| // dependencies, it shouldn't fail unless that fails. |
| log.exception(error, stackTrace); |
| } |
| } |
| } |
| |
| /// All executables that should be snapshotted from this entrypoint. |
| /// |
| /// This is all executables in direct dependencies. |
| /// that don't transitively depend on [this] or on a mutable dependency. |
| /// |
| /// Except globally activated packages they should precompile executables from |
| /// the package itself if they are immutable. |
| List<Executable> get _builtExecutables { |
| if (isGlobal) { |
| if (isCached) { |
| return root.executablePaths |
| .map((path) => Executable(root.name, path)) |
| .toList(); |
| } else { |
| return <Executable>[]; |
| } |
| } |
| final r = root.immediateDependencies.keys.expand((packageName) { |
| final package = packageGraph.packages[packageName]!; |
| return package.executablePaths |
| .map((path) => Executable(packageName, path)); |
| }).toList(); |
| return r; |
| } |
| |
| /// Precompiles all [_builtExecutables]. |
| Future<void> precompileExecutables() async { |
| migrateCache(); |
| |
| final executables = _builtExecutables; |
| |
| if (executables.isEmpty) return; |
| |
| await log.progress('Building package executables', () async { |
| if (isGlobal) { |
| /// Global snapshots might linger in the cache if we don't remove old |
| /// snapshots when it is re-activated. |
| cleanDir(_snapshotPath); |
| } else { |
| ensureDir(_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 .dart file at [path] to a snapshot. |
| Future<void> precompileExecutable(Executable executable) async { |
| await log.progress('Building package executable', () async { |
| ensureDir(p.dirname(pathOfExecutable(executable))); |
| return waitAndPrintErrors([_precompileExecutable(executable)]); |
| }); |
| } |
| |
| Future<void> _precompileExecutable(Executable executable) async { |
| final package = executable.package; |
| |
| await dart.precompile( |
| executablePath: resolveExecutable(executable), |
| outputPath: pathOfExecutable(executable), |
| incrementalDillPath: incrementalDillPathOfExecutable(executable), |
| packageConfigPath: packageConfigFile, |
| name: |
| '$package:${p.basenameWithoutExtension(executable.relativePath)}'); |
| } |
| |
| /// The location of the snapshot of the dart program at [path] in [package] |
| /// will be stored here. |
| /// |
| /// We use the sdk version to make sure we don't run snapshots from a |
| /// different sdk. |
| /// |
| /// [path] must be relative. |
| String pathOfExecutable(Executable executable) { |
| assert(p.isRelative(executable.relativePath)); |
| final versionSuffix = sdk.version; |
| return isGlobal |
| ? p.join(_snapshotPath, |
| '${p.basename(executable.relativePath)}-$versionSuffix.snapshot') |
| : p.join(_snapshotPath, executable.package, |
| '${p.basename(executable.relativePath)}-$versionSuffix.snapshot'); |
| } |
| |
| String incrementalDillPathOfExecutable(Executable executable) { |
| assert(p.isRelative(executable.relativePath)); |
| return isGlobal |
| ? p.join(_incrementalDillsPath, |
| '${p.basename(executable.relativePath)}.incremental.dill') |
| : p.join(_incrementalDillsPath, executable.package, |
| '${p.basename(executable.relativePath)}.incremental.dill'); |
| } |
| |
| /// The absolute path of [executable] resolved relative to [this]. |
| String resolveExecutable(Executable executable) { |
| return p.join( |
| packageGraph.packages[executable.package]!.dir, |
| executable.relativePath, |
| ); |
| } |
| |
| /// Deletes outdated cached executable snapshots. |
| /// |
| /// If [changed] is passed, only dependencies whose contents might be changed |
| /// if one of the given packages changes will have their executables deleted. |
| void _deleteExecutableSnapshots({Iterable<String>? changed}) { |
| if (!dirExists(_snapshotPath)) return; |
| |
| // If we don't know what changed, we can't safely re-use any snapshots. |
| if (changed == null) { |
| deleteEntry(_snapshotPath); |
| return; |
| } |
| var changedDeps = changed; |
| changedDeps = changedDeps.toSet(); |
| |
| // If the existing executable was compiled with a different SDK, we need to |
| // recompile regardless of what changed. |
| // TODO(nweiz): Use the VM to check this when issue 20802 is fixed. |
| var sdkVersionPath = p.join(_snapshotPath, 'sdk-version'); |
| if (!fileExists(sdkVersionPath) || |
| readTextFile(sdkVersionPath) != '${sdk.version}\n') { |
| deleteEntry(_snapshotPath); |
| return; |
| } |
| |
| // Clean out any outdated snapshots. |
| for (var entry in listDir(_snapshotPath)) { |
| if (!dirExists(entry)) continue; |
| |
| var package = p.basename(entry); |
| if (!packageGraph.packages.containsKey(package) || |
| packageGraph.isPackageMutable(package) || |
| packageGraph |
| .transitiveDependencies(package) |
| .any((dep) => changedDeps.contains(dep.name))) { |
| deleteEntry(entry); |
| } |
| } |
| } |
| |
| /// Throws a [DataError] if the `.dart_tool/package_config.json` file doesn't |
| /// exist or if it's out-of-date relative to the lockfile or the pubspec. |
| /// |
| /// A `.packages` file is not required. But if it exists it is checked for |
| /// consistency with the pubspec.lock. |
| void assertUpToDate() { |
| if (isCached) return; |
| |
| if (!entryExists(lockFilePath)) { |
| dataError( |
| 'No $lockFilePath file found, please run "$topLevelProgram pub get" first.'); |
| } |
| if (!entryExists(packageConfigFile)) { |
| dataError( |
| 'No $packageConfigFile file found, please run "$topLevelProgram pub get".\n' |
| '\n' |
| 'Starting with Dart 2.7, the package_config.json file configures ' |
| 'resolution of package import URIs; run "$topLevelProgram pub get" to generate it.', |
| ); |
| } |
| |
| // Manually parse the lockfile because a full YAML parse is relatively slow |
| // and this is on the hot path for "pub run". |
| var lockFileText = readTextFile(lockFilePath); |
| var hasPathDependencies = lockFileText.contains('\n source: path\n'); |
| |
| var pubspecModified = File(pubspecPath).lastModifiedSync(); |
| var lockFileModified = File(lockFilePath).lastModifiedSync(); |
| |
| var pubspecChanged = lockFileModified.isBefore(pubspecModified); |
| var pubspecOverridesChanged = false; |
| |
| if (hasPubspecOverrides) { |
| var pubspecOverridesModified = |
| File(pubspecOverridesPath).lastModifiedSync(); |
| pubspecOverridesChanged = |
| lockFileModified.isBefore(pubspecOverridesModified); |
| } |
| |
| var touchedLockFile = false; |
| if (pubspecChanged || pubspecOverridesChanged || hasPathDependencies) { |
| // If `pubspec.lock` is older than `pubspec.yaml` or |
| // `pubspec_overrides.yaml`, or we have path dependencies, then we check |
| // that `pubspec.lock` is a correct solution for the requirements in |
| // `pubspec.yaml` and `pubspec_overrides.yaml`. This aims to: |
| // * Prevent missing packages when `pubspec.lock` is checked into git. |
| // * Mitigate missing transitive dependencies when the `pubspec.yaml` in |
| // a path dependency is changed. |
| _assertLockFileUpToDate(); |
| if (_arePackagesAvailable()) { |
| touchedLockFile = true; |
| touch(lockFilePath); |
| } else { |
| var filePath = pubspecChanged ? pubspecPath : pubspecOverridesPath; |
| dataError('The $filePath file has changed since the $lockFilePath ' |
| 'file was generated, please run "$topLevelProgram pub get" again.'); |
| } |
| } |
| |
| if (fileExists(packagesFile)) { |
| var packagesModified = File(packagesFile).lastModifiedSync(); |
| if (packagesModified.isBefore(lockFileModified)) { |
| _checkPackagesFileUpToDate(); |
| touch(packagesFile); |
| } else if (touchedLockFile) { |
| touch(packagesFile); |
| } |
| } |
| |
| var packageConfigModified = File(packageConfigFile).lastModifiedSync(); |
| if (packageConfigModified.isBefore(lockFileModified) || |
| hasPathDependencies) { |
| // If `package_config.json` is older than `pubspec.lock` or we have |
| // path dependencies, then we check that `package_config.json` is a |
| // correct configuration on the local machine. This aims to: |
| // * Mitigate issues when copying a folder from one machine to another. |
| // * Force `pub get` if a path dependency has changed language version. |
| _checkPackageConfigUpToDate(); |
| touch(packageConfigFile); |
| } else if (touchedLockFile) { |
| touch(packageConfigFile); |
| } |
| |
| for (var match in _sdkConstraint.allMatches(lockFileText)) { |
| var identifier = match[1] == 'sdk' ? 'dart' : match[1]!.trim(); |
| var sdk = sdks[identifier]!; |
| |
| // Don't complain if there's an SDK constraint for an unavailable SDK. For |
| // example, the Flutter SDK being unavailable just means that we aren't |
| // running from within the `flutter` executable, and we want users to be |
| // able to `pub run` non-Flutter tools even in a Flutter app. |
| if (!sdk.isAvailable) continue; |
| |
| var parsedConstraint = VersionConstraint.parse(match[2]!); |
| if (!parsedConstraint.allows(sdk.version!)) { |
| dataError('${sdk.name} ${sdk.version} is incompatible with your ' |
| "dependencies' SDK constraints. Please run \"$topLevelProgram pub get\" again."); |
| } |
| } |
| } |
| |
| /// Determines whether or not the lockfile is out of date with respect to the |
| /// pubspec. |
| /// |
| /// If any mutable pubspec contains dependencies that are not in the lockfile |
| /// or that don't match what's in there, this will throw a [DataError] |
| /// describing the issue. |
| void _assertLockFileUpToDate() { |
| if (!root.immediateDependencies.values.every(_isDependencyUpToDate)) { |
| dataError( |
| 'The $pubspecPath file has changed since the $lockFilePath file ' |
| 'was generated, please run "$topLevelProgram pub get" again.'); |
| } |
| |
| var overrides = MapKeySet(root.dependencyOverrides); |
| |
| // 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) => |
| overrides.contains(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'); |
| dataError('$relativePubspecPath has ' |
| 'changed since the $lockFilePath file was generated, please run ' |
| '"$topLevelProgram pub get" again.'); |
| } |
| } |
| |
| /// Returns whether the locked version of [dep] matches the dependency. |
| bool _isDependencyUpToDate(PackageRange dep) { |
| if (dep.name == root.name) return true; |
| |
| var locked = lockFile.packages[dep.name]; |
| return locked != null && dep.allows(locked); |
| } |
| |
| /// Determines whether all of the packages in the lockfile are already |
| /// installed and available. |
| bool _arePackagesAvailable() { |
| return lockFile.packages.values.every((package) { |
| if (package.source is UnknownSource) return false; |
| |
| // We only care about cached sources. Uncached sources aren't "installed". |
| // If one of those is missing, we want to show the user the file not |
| // found error later since installing won't accomplish anything. |
| var source = package.source; |
| if (source is! CachedSource) return true; |
| |
| // Get the directory. |
| var dir = cache.getDirectory(package, relativeFrom: '.'); |
| // See if the directory is there and looks like a package. |
| return fileExists(p.join(dir, 'pubspec.yaml')); |
| }); |
| } |
| |
| /// Determines [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, |
| ) { |
| // 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 packageName == root.name || |
| lockFile.packages.containsKey(packageName); |
| }); |
| if (hasExtraMappings) { |
| 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 = root.path( |
| cache.getDirectory(lockFileId, relativeFrom: root.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; |
| }); |
| } |
| |
| /// Checks whether or not the `.packages` file is out of date with respect |
| /// to the [lockfile]. |
| /// |
| /// This will throw a [DataError] if [lockfile] contains dependencies that |
| /// are not in the `.packages` or that don't match what's in there. |
| void _checkPackagesFileUpToDate() { |
| void outOfDate() { |
| dataError('The $lockFilePath file has changed since the .packages file ' |
| 'was generated, please run "$topLevelProgram pub get" again.'); |
| } |
| |
| var packages = packages_file.parse( |
| File(packagesFile).readAsBytesSync(), p.toUri(packagesFile)); |
| |
| final packagePathsMapping = <String, String>{}; |
| for (final package in packages.keys) { |
| final packageUri = packages[package]!; |
| |
| // Pub only generates "file:" and relative URIs. |
| if (packageUri.scheme != 'file' && packageUri.scheme.isNotEmpty) { |
| outOfDate(); |
| } |
| |
| // Get the dirname of the .packages path, since it's pointing to lib/. |
| final packagePath = p.dirname(p.join(root.dir, p.fromUri(packageUri))); |
| packagePathsMapping[package] = packagePath; |
| } |
| |
| if (!_isPackagePathsMappingUpToDateWithLockfile(packagePathsMapping)) { |
| outOfDate(); |
| } |
| } |
| |
| /// Checks whether or not the `.dart_tool/package_config.json` file is |
| /// out of date with respect to the lockfile. |
| /// |
| /// This will throw a [DataError] if the [lockfile] contains dependencies that |
| /// are not in the `.dart_tool/package_config.json` or that don't match |
| /// what's in there. |
| /// |
| /// Throws [DataException], if `.dart_tool/package_config.json` is not |
| /// up-to-date for some other reason. |
| void _checkPackageConfigUpToDate() { |
| void outOfDate() { |
| dataError('The $lockFilePath file has changed since the ' |
| '$packageConfigFile file ' |
| 'was generated, please run "$topLevelProgram pub get" again.'); |
| } |
| |
| void badPackageConfig() { |
| dataError('The "$packageConfigFile" file is not recognized by ' |
| '"pub" version, please run "$topLevelProgram pub get".'); |
| } |
| |
| late String packageConfigRaw; |
| try { |
| packageConfigRaw = readTextFile(packageConfigFile); |
| } on FileException { |
| dataError( |
| 'The "$packageConfigFile" file does not exist, please run "$topLevelProgram pub get".'); |
| } |
| |
| late PackageConfig cfg; |
| try { |
| cfg = PackageConfig.fromJson(json.decode(packageConfigRaw)); |
| } 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 (cfg.configVersion != 2) { |
| badPackageConfig(); |
| } |
| |
| final packagePathsMapping = <String, String>{}; |
| |
| // We allow the package called 'flutter_gen' to be injected into |
| // package_config. |
| // |
| // This is somewhat a hack. But it allows flutter to generate code in a |
| // package as it likes. |
| // |
| // See https://github.com/flutter/flutter/issues/73870 . |
| final packagesToCheck = |
| cfg.packages.where((package) => package.name != 'flutter_gen'); |
| for (final pkg in packagesToCheck) { |
| // Pub always makes a packageUri of lib/ |
| if (pkg.packageUri == null || pkg.packageUri.toString() != 'lib/') { |
| badPackageConfig(); |
| } |
| packagePathsMapping[pkg.name] = |
| root.path('.dart_tool', p.fromUri(pkg.rootUri)); |
| } |
| if (!_isPackagePathsMappingUpToDateWithLockfile(packagePathsMapping)) { |
| outOfDate(); |
| } |
| |
| // 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 cfg.packages) { |
| if (pkg.name == root.name || pkg.name == 'flutter_gen') 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 = LanguageVersion.fromSdkConstraint( |
| cache.load(id).pubspec.sdkConstraints[sdk.identifier], |
| ); |
| if (pkg.languageVersion != languageVersion) { |
| final relativePubspecPath = p.join( |
| cache.getDirectory(id, relativeFrom: '.'), |
| 'pubspec.yaml', |
| ); |
| dataError('$relativePubspecPath has ' |
| 'changed since the $lockFilePath file was generated, please run ' |
| '"$topLevelProgram pub get" again.'); |
| } |
| } on FileException { |
| dataError('Failed to read pubspec.yaml for "${pkg.name}", perhaps the ' |
| 'entry is missing, please run "$topLevelProgram pub get".'); |
| } |
| } |
| } |
| |
| /// Saves a list of concrete package versions to the `pubspec.lock` file. |
| /// |
| /// Will use Windows line endings (`\r\n`) if a `pubspec.lock` exists, and |
| /// uses that. |
| void saveLockFile(SolveResult result) { |
| _lockFile = result.lockFile; |
| |
| final windowsLineEndings = fileExists(lockFilePath) && |
| detectWindowsLineEndings(readTextFile(lockFilePath)); |
| |
| final serialized = lockFile.serialize(root.dir); |
| writeTextFile(lockFilePath, |
| windowsLineEndings ? serialized.replaceAll('\n', '\r\n') : serialized); |
| } |
| |
| /// If the entrypoint uses the old-style `.pub` cache directory, migrates it |
| /// to the new-style `.dart_tool/pub` directory. |
| void migrateCache() { |
| // Cached packages don't have these. |
| if (isCached) return; |
| |
| var oldPath = p.join(_configRoot!, '.pub'); |
| if (!dirExists(oldPath)) return; |
| |
| var newPath = root.path('.dart_tool/pub'); |
| |
| // If both the old and new directories exist, something weird is going on. |
| // Do nothing to avoid making things worse. Pub will prefer the new |
| // directory anyway. |
| if (dirExists(newPath)) return; |
| |
| ensureDir(p.dirname(newPath)); |
| renameDir(oldPath, newPath); |
| } |
| |
| /// We require an SDK constraint lower-bound as of Dart 2.12.0 |
| /// |
| /// We don't allow unknown sdks. |
| void _checkSdkConstraint(Pubspec pubspec) { |
| final dartSdkConstraint = pubspec.sdkConstraints['dart']; |
| if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) { |
| // Suggest version range '>=2.10.0 <3.0.0', we avoid using: |
| // [CompatibleWithVersionRange] because some pub versions don't support |
| // caret syntax (e.g. '^2.10.0') |
| var suggestedConstraint = VersionRange( |
| min: Version.parse('2.10.0'), |
| max: Version.parse('2.10.0').nextBreaking, |
| includeMin: true, |
| ); |
| // 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, |
| ); |
| } |
| throw DataException(''' |
| $pubspecPath has no lower-bound SDK constraint. |
| You should edit $pubspecPath to contain an SDK constraint: |
| |
| environment: |
| sdk: '$suggestedConstraint' |
| |
| See https://dart.dev/go/sdk-constraint |
| '''); |
| } |
| 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 PubspecException(''' |
| $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); |
| } |
| } |
| } |
| } |
| |
| /// Returns `true` if the [text] looks like it uses windows line endings. |
| /// |
| /// The heuristic used is to count all `\n` in the text and if stricly more than |
| /// half of them are preceded by `\r` we report `true`. |
| @visibleForTesting |
| bool detectWindowsLineEndings(String text) { |
| var index = -1; |
| var unixNewlines = 0; |
| var windowsNewlines = 0; |
| while ((index = text.indexOf('\n', index + 1)) != -1) { |
| if (index != 0 && text[index - 1] == '\r') { |
| windowsNewlines++; |
| } else { |
| unixNewlines++; |
| } |
| } |
| return windowsNewlines > unixNewlines; |
| } |