blob: 2416edc4b5d6eae325a5d7026ba8b4a162d5e399 [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 '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 'pub_embeddable_command.dart';
import 'pubspec.dart';
import 'sdk.dart';
import 'solver.dart';
import 'solver/report.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 {
try {
return _lockFile = 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 .
late PackageConfig packageConfig = () {
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));
} 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.
PackageGraph get packageGraph => _packageGraph ??= _createPackageGraph();
PackageGraph _createPackageGraph() {
assertUpToDate();
var packages = {
for (var packageEntry in packageConfig.nonInjectedPackages)
packageEntry.name: Package.load(
packageEntry.name,
packageEntry.resolvedRootDir(packageConfigPath),
cache.sources,
),
};
packages[root.name] = root;
return PackageGraph(this, 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
/// relative to the current working directory .
late String packageConfigPath = p.relative(
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 {
if (p.isWithin(cache.rootDir, rootDir)) {
fail('Cannot operate on packages inside the cache.');
}
}
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> writePackageConfigFile() async {
final entrypointName = isGlobal ? null : root.name;
ensureDir(p.dirname(packageConfigPath));
writeTextFile(
packageConfigPath,
await lockFile.packageConfigFile(
cache,
entrypoint: entrypointName,
entrypointSdkConstraint:
root.pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
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 [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,
required PubAnalytics? analytics,
bool summaryOnly = false,
bool enforceLockfile = false,
}) async {
summaryOnly = summaryOnly || _summaryOnlyEnvironment;
final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
String forDetails() {
if (!summaryOnly) return '';
final enforceLockfileOption =
enforceLockfile ? ' --enforce-lockfile' : '';
final directoryOption =
root.isInMemory || root.dir == '.' ? '' : ' --directory ${root.dir}';
return ' For details run `$topLevelProgram pub ${type.toString()}$directoryOption$enforceLockfileOption`';
}
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 {
_checkSdkConstraint(root.pubspec);
return resolveVersions(
type,
cache,
root,
lockFile: lockFile,
unlock: unlock ?? [],
);
});
} catch (e) {
if (summaryOnly && (e is ApplicationException)) {
throw ApplicationException(
'Resolving dependencies$suffix failed.${forDetails()}',
);
} else {
rethrow;
}
}
// 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,
root,
lockFile,
newLockFile,
result.availableVersions,
cache,
dryRun: dryRun,
enforceLockfile: enforceLockfile,
quiet: summaryOnly,
);
final hasChanges = await report.show();
await report.summarize();
if (enforceLockfile && hasChanges) {
var suggestion = summaryOnly
? ''
: '''
\n\nTo update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
`--enforce-lockfile`.''';
dataError('''
Unable to satisfy `$pubspecPath` using `$lockFilePath`$suffix.${forDetails()}$suggestion''');
}
if (!(dryRun || enforceLockfile)) {
newLockFile.writeToFile(lockFilePath, cache);
}
_lockFile = newLockFile;
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 writePackageConfigFile();
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.
///
/// 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(pathOfExecutable(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: resolveExecutable(executable),
outputPath: pathOfExecutable(executable),
incrementalDillPath: incrementalDillPathOfExecutable(executable),
packageConfigPath: packageConfigPath,
name: '$package:${p.basenameWithoutExtension(executable.relativePath)}',
additionalSources: additionalSources,
nativeAssets: nativeAssets,
);
}
/// 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.
///
/// If [checkForSdkUpdate] is `true`, the resolution is considered outdated if
/// the package_config.json was created by a different sdk. See
/// [_checkPackageConfigSameDartSdk].
/// TODO(sigurdm): we should consider if we can instead in all places update
/// the resolution automatically.
void assertUpToDate({bool checkForSdkUpdate = false}) {
if (isCached) return;
if (!entryExists(lockFilePath)) {
dataError(
'No $lockFilePath file found, please run "$topLevelProgram pub get" first.',
);
}
if (!entryExists(packageConfigPath)) {
dataError(
'No $packageConfigPath 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.');
}
}
var packageConfigModified = File(packageConfigPath).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(packageConfigPath);
} else {
if (touchedLockFile) {
touch(packageConfigPath);
}
}
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.");
}
}
// We want to do ensure a pub get gets run when updating a minor version of
// the Dart SDK.
//
// Putting this check last because it leads to less specific messages than
// the 'incompatible sdk' check above.
if (checkForSdkUpdate) _checkPackageConfigSameDartSdk();
}
/// 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 `.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 '
'$packageConfigPath file '
'was generated, please run "$topLevelProgram pub get" again.');
}
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/') {
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 packageConfig.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 = cache.load(id).pubspec.languageVersion;
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".');
}
}
}
/// Checks whether or not the `.dart_tool/package_config.json` file is was
/// generated by a different sdk down changes in minor versions.
///
/// Throws [DataException], if `.dart_tool/package_config.json` the version
/// changed sufficiently.
void _checkPackageConfigSameDartSdk() {
final generatorVersion = packageConfig.generatorVersion;
if (generatorVersion == null ||
generatorVersion.major != sdk.version.major ||
generatorVersion.minor != sdk.version.minor) {
dataError('The sdk was updated since last package resolution. Please run '
'"$topLevelProgram pub get" again.');
}
}
/// 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.dartSdkConstraint.effectiveConstraint;
if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) {
// 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,
);
}
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
''');
}
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,
);
}
}
}
Never badPackageConfig() {
dataError('The "$packageConfigPath" file is not recognized by '
'"pub" version, please run "$topLevelProgram pub get".');
}
/// 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';
}