blob: 562d6af9bd49a7039facbe7965d3215ec2bfda37 [file] [log] [blame]
// 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:path/path.dart' as p;
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 '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_graph.dart';
import 'package_name.dart';
import 'pubspec.dart';
import 'sdk.dart';
import 'sdk/flutter.dart';
import 'solver.dart';
import 'solver/report.dart';
import 'solver/solve_suggestions.dart';
import 'source/cached.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: RootDescription(dir),
allowOverridesFile: true,
);
} on FileException {
continue;
}
pubspecsMet[p.canonicalize(dir)] = pubspec;
final Package root;
if (pubspec.resolution == Resolution.none) {
root = Package.load(
dir,
cache.sources,
loadPubspec: (
path, {
expectedName,
required withPubspecOverrides,
}) =>
pubspecsMet[p.canonicalize(path)] ??
Pubspec.load(
path,
cache.sources,
expectedName: expectedName,
allowOverridesFile: withPubspecOverrides,
containingDescription: RootDescription(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) {
throw FileException(
'Found no `pubspec.yaml` file in `${p.normalize(p.absolute(workingDir))}` 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 [DataError] 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".');
}
late String packageConfigRaw;
try {
packageConfigRaw = readTextFile(packageConfigPath);
} on FileException {
dataError(
'The "$packageConfigPath" file does not exist, please run "$topLevelProgram pub get".',
);
}
late 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 [DataError] 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);
var packages = {
for (var packageEntry in packageConfig.nonInjectedPackages)
packageEntry.name: Package.load(
packageEntry.resolvedRootDir(packageConfigPath),
cache.sources,
expectedName: packageEntry.name,
),
};
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 String packageConfigPath = p.relative(
p.normalize(p.join(workspaceRoot.dir, '.dart_tool', 'package_config.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 dependency executable snapshots.
String get _snapshotPath => p.join(
isCachedGlobal
? workspaceRoot.dir
: p.join(workspaceRoot.dir, '.dart_tool/pub'),
'bin',
);
Entrypoint._(
this.workingDir,
this._lockFile,
this._example,
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
/// [rootDir] 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
/// [updatedPubspec] replacing the with one for the corresponding package.
Entrypoint withUpdatedPubspecs(Map<Package, Pubspec> updatedPubspecs) {
final existingPubspecs = <String, Pubspec>{};
// First extract all pubspecs from the workspace.
for (final package in workspaceRoot.transitiveWorkspace) {
existingPubspecs[package.dir] =
updatedPubspecs[package] ?? package.pubspec;
}
final newWorkspaceRoot = Package.load(
workspaceRoot.dir,
cache.sources,
loadPubspec: (
dir, {
expectedName,
required withPubspecOverrides,
}) =>
existingPubspecs[dir] ??
Pubspec.load(
dir,
cache.sources,
containingDescription: RootDescription(dir),
),
);
final newWorkPackage = newWorkspaceRoot.transitiveWorkspace
.firstWhere((package) => package.dir == workPackage.dir);
return Entrypoint._(
workingDir,
_lockFile,
_example,
_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 withUpdatedPubspecs({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 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(workspaceRoot.path('example', 'pubspec.yaml'))) {
return null;
}
return _example = Entrypoint(workspaceRoot.path('example'), cache);
}
Entrypoint? _example;
/// Writes the .dart_tool/package_config.json file
Future<void> writePackageConfigFile() async {
ensureDir(p.dirname(packageConfigPath));
writeTextFile(
packageConfigPath,
await _packageConfigFile(
cache,
entrypointSdkConstraint: workspaceRoot
.pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
),
);
}
/// Returns the contents of the `.dart_tool/package_config` file generated
/// from this entrypoint based on [lockFile].
///
/// If [isCachedGlobal] no entry will be created for [workspaceRoot].
Future<String> _packageConfigFile(
SystemCache cache, {
VersionConstraint? entrypointSdkConstraint,
}) async {
final entries = <PackageConfigEntry>[];
late final relativeFromPath = p.join(workspaceRoot.dir, '.dart_tool');
for (final name in ordered(lockFile.packages.keys)) {
final id = lockFile.packages[name]!;
final rootPath = cache.getDirectory(id, relativeFrom: relativeFromPath);
final pubspec = await cache.describe(id);
entries.add(
PackageConfigEntry(
name: name,
rootUri: p.toUri(rootPath),
packageUri: p.toUri('lib/'),
languageVersion: pubspec.languageVersion,
),
);
}
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,
),
);
}
}
final packageConfig = PackageConfig(
configVersion: 2,
packages: entries,
generated: DateTime.now(),
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(),
},
);
return '${JsonEncoder.withIndent(' ').convert(packageConfig.toJson())}\n';
}
/// 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.
///
/// 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 [summaryOnly] is `true` only success or failure will be
/// shown --- in case of failure, a reproduction command is shown.
///
/// Updates [lockFile] and [packageRoot] 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,
bool dryRun = false,
bool precompile = false,
bool summaryOnly = false,
bool enforceLockfile = false,
}) async {
workspaceRoot; // This will throw early if pubspec.yaml could not be found.
summaryOnly = summaryOnly || _summaryOnlyEnvironment;
final suffix = workspaceRoot.dir == '.' ? '' : ' in `${workspaceRoot.dir}`';
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 ?? [],
);
});
} on SolveFailure catch (e) {
throw SolveFailure(
e.incompatibility,
suggestions: await suggestResolutionAlternatives(
this,
type,
e.incompatibility,
unlock ?? [],
cache,
),
);
}
// 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);
final report = SolveReport(
type,
workspaceRoot.dir,
workspaceRoot.pubspec,
lockFile,
newLockFile,
result.availableVersions,
cache,
dryRun: dryRun,
enforceLockfile: enforceLockfile,
quiet: summaryOnly,
);
await report.show(summary: true);
if (enforceLockfile && !_lockfilesMatch(lockFile, newLockFile)) {
dataError('''
Unable to satisfy `${workspaceRoot.pubspecPath}` using `$lockFilePath`$suffix.
To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
`--enforce-lockfile`.''');
}
if (!(dryRun || enforceLockfile)) {
newLockFile.writeToFile(lockFilePath, cache);
}
_lockFile = newLockFile;
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.fromSolveResult(this, result));
await writePackageConfigFile();
try {
if (precompile) {
await precompileExecutables();
} else {
await _deleteExecutableSnapshots();
}
} 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.
Future<List<Executable>> get _builtExecutables async {
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 {
final executables = await _builtExecutables;
if (executables.isEmpty) return;
await log.progress('Building package executables', () async {
if (isCachedGlobal) {
/// 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.
///
/// The [additionalSources], if provided, instruct the compiler to include
/// additional source files into compilation even if they are not referenced
/// from the main library.
///
/// The [nativeAssets], if provided, instruct the compiler include a native
/// assets map.
Future<void> precompileExecutable(
Executable executable, {
List<String> additionalSources = const [],
String? nativeAssets,
}) async {
await log.progress('Building package executable', () async {
ensureDir(p.dirname(pathOfSnapshot(executable)));
return waitAndPrintErrors([
_precompileExecutable(
executable,
additionalSources: additionalSources,
nativeAssets: nativeAssets,
),
]);
});
}
Future<void> _precompileExecutable(
Executable executable, {
List<String> additionalSources = const [],
String? nativeAssets,
}) async {
final package = executable.package;
await dart.precompile(
executablePath: executable.resolve(packageConfig, packageConfigPath),
outputPath: pathOfSnapshot(executable),
packageConfigPath: packageConfigPath,
name: '$package:${p.basenameWithoutExtension(executable.relativePath)}',
additionalSources: additionalSources,
nativeAssets: nativeAssets,
);
cache.maintainCache();
}
/// 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 pathOfSnapshot(Executable executable) {
return isCachedGlobal
? executable.pathOfGlobalSnapshot(workspaceRoot.dir)
: executable.pathOfSnapshot(workspaceRoot.dir);
}
/// Deletes cached snapshots that are from a different sdk.
Future<void> _deleteExecutableSnapshots() async {
if (!dirExists(_snapshotPath)) return;
// Clean out any outdated snapshots.
for (var entry in listDir(_snapshotPath)) {
if (!fileExists(entry)) {
// Not a file
continue;
}
if (!entry.endsWith('${sdk.version}.snapshot')) {
// Made with a different sdk version. Clean it up.
deleteEntry(entry);
}
}
}
/// Does a fast-pass check to see if the resolution is up-to-date
/// ([_isUpToDate]). If not, run a resolution with `pub get` semantics.
///
/// If [summaryOnly] is `true` (the default) only a short summary is shown of
/// the solve.
///
/// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
/// output if no terminal is attached.
static Future<PackageConfig> ensureUpToDate(
String dir, {
required SystemCache cache,
bool summaryOnly = true,
bool onlyOutputWhenTerminal = true,
}) async {
final lockFilePath = p.normalize(p.join(dir, 'pubspec.lock'));
final packageConfigPath =
p.normalize(p.join(dir, '.dart_tool', 'package_config.json'));
/// 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) {
/// 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);
}
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;
}
}
}
if (!root.immediateDependencies.values.every(isDependencyUpToDate)) {
final pubspecPath = p.normalize(p.join(dir, 'pubspec.yaml'));
log.fine(
'The $pubspecPath file has changed since the $lockFilePath file '
'was generated.');
return false;
}
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');
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,
) {
/// 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,
) {
// 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) {
log.fine(packagePathsMapping.toString());
log.fine(lockFile.packages.toString());
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;
});
}
final packagePathsMapping = <String, String>{};
final packagesToCheck = packageConfig.nonInjectedPackages;
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] =
root.path('.dart_tool', p.fromUri(pkg.rootUri));
}
if (!isPackagePathsMappingUpToDateWithLockfile(packagePathsMapping)) {
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.nonInjectedPackages) {
if (pkg.name == root.name) 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;
}
/// The [PackageConfig] object representing `.dart_tool/package_config.json`
/// if it and `pubspec.lock` exist and are up to date with respect to
/// pubspec.yaml and its dependencies. Or `null` if it is outdate
///
/// 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 accomodate for cases
/// where version control or other processes mess up the timestamp order.
///
/// If the resolution is still valid, the timestamps are updated and this
/// returns `true`. Otherwise this returns `false`.
///
/// 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 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.
PackageConfig? isResolutionUpToDate() {
late final packageConfig = _loadPackageConfig(packageConfigPath);
if (p.isWithin(cache.rootDir, packageConfigPath)) {
// We always consider a global package (inside the cache) up-to-date.
return packageConfig;
}
/// Whether or not the `.dart_tool/package_config.json` file is 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 packageConfigStat = tryStatFile(packageConfigPath);
if (packageConfigStat == null) {
log.fine('No $packageConfigPath file found".\n');
return null;
}
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 ? null : flutter.version)) {
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) {
log.fine(
'The pub cache has moved from ${packageConfig.additionalProperties['pubCache']} 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;
// 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(
'.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 (pubspecStat.modified.isAfter(lockFileModified)) {
log.fine('`$pubspecPath` is newer than `$lockFilePath`');
lockfileNewerThanPubspecs = false;
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 (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
lockfileNewerThanPubspecs = false;
}
}
}
var touchedLockFile = false;
late final lockFile = _loadLockFile(lockFilePath, cache);
late final root = Package.load(dir, cache.sources);
if (!lockfileNewerThanPubspecs) {
if (isLockFileUpToDate(lockFile, root)) {
touch(lockFilePath);
touchedLockFile = true;
} else {
return null;
}
}
if (touchedLockFile ||
lockFileModified.isAfter(packageConfigStat.modified)) {
log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
if (isPackageConfigUpToDate(packageConfig, lockFile, root)) {
touch(packageConfigPath);
} else {
return null;
}
}
return packageConfig;
}
switch (isResolutionUpToDate()) {
case null:
final entrypoint = Entrypoint(
dir, cache,
// [ensureUpToDate] is also used for entries in 'global_packages/'
checkInCache: false,
);
if (onlyOutputWhenTerminal) {
await log.errorsOnlyUnlessTerminal(() async {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
});
} else {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
}
return entrypoint.packageConfig;
case PackageConfig packageConfig:
log.fine('Package Config up to date.');
return packageConfig;
}
}
/// 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';
/// Returns true if the packages in [newLockFile] and [previousLockFile] are
/// all the same, meaning:
/// * same set of package-names
/// * for each package
/// * same version number
/// * same resolved description (same content-hash, git hash, path)
bool _lockfilesMatch(LockFile previousLockFile, LockFile newLockFile) {
if (previousLockFile.packages.length != newLockFile.packages.length) {
return false;
}
for (final package in newLockFile.packages.values) {
final oldPackage = previousLockFile.packages[package.name];
if (oldPackage == null) return false; // Package added to resolution.
if (oldPackage.version != package.version) return false;
if (oldPackage.description != package.description) return false;
}
return true;
}
/// Remove any `pubspec.lock` or `.dart_tool/package_config.json` files in
/// workspace packages that are not the root package.
///
/// 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() {
for (final package in workspaceRoot.transitiveWorkspace) {
if (package.pubspec.resolution == Resolution.workspace) {
deleteEntry(p.join(package.dir, 'pubspec.lock'));
deleteEntry(p.join(package.dir, '.dart_tool', 'package_config.json'));
}
}
}
}