ensureUpToDate static method
Future<({PackageConfig packageConfig, String rootDir})>
ensureUpToDate(
- String dir, {
- required SystemCache cache,
- bool summaryOnly = true,
- bool onlyOutputWhenTerminal = true,
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.
When succesfull returns the found/created PackageConfig
and the
directory containing it.
Implementation
static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
String dir, {
required SystemCache cache,
bool summaryOnly = true,
bool onlyOutputWhenTerminal = true,
}) async {
late final wasRelative = p.isRelative(dir);
String relativeIfNeeded(String path) =>
wasRelative ? p.relative(path) : path;
/// Whether the lockfile is out of date with respect to the dependencies'
/// pubspecs.
///
/// If any mutable pubspec contains dependencies that are not in the lockfile
/// or that don't match what's in there, this will return `false`.
bool isLockFileUpToDate(
LockFile lockFile,
Package root, {
required String lockFilePath,
}) {
/// Returns whether the locked version of [dep] matches the dependency.
bool isDependencyUpToDate(PackageRange dep) {
if (dep.name == root.name) return true;
final locked = lockFile.packages[dep.name];
return locked != null && dep.allows(locked);
}
for (final MapEntry(key: sdkName, value: constraint)
in lockFile.sdkConstraints.entries) {
final sdk = sdks[sdkName];
if (sdk == null) {
log.fine('Unknown sdk $sdkName in `$lockFilePath`');
return false;
}
if (!sdk.isAvailable) {
log.fine('sdk: ${sdk.name} not available');
return false;
}
final sdkVersion = sdk.version;
if (sdkVersion != null) {
if (!constraint.effectiveConstraint.allows(sdkVersion)) {
log.fine(
'`$lockFilePath` requires $sdkName $constraint. Current version is $sdkVersion',
);
return false;
}
}
}
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;
}
// 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) =>
root.allOverridesInWorkspace.containsKey(dep.name) ||
isDependencyUpToDate(dep),
)) {
continue;
}
} on FileException {
// If we can't load the pubspec, the user needs to re-run "pub get".
}
final relativePubspecPath =
p.join(cache.getDirectory(id, relativeFrom: '.'), 'pubspec.yaml');
log.fine('$relativePubspecPath has '
'changed since the $lockFilePath file was generated.');
return false;
}
return true;
}
/// Whether or not the `.dart_tool/package_config.json` file is
/// out of date with respect to the lockfile.
bool isPackageConfigUpToDate(
PackageConfig packageConfig,
LockFile lockFile,
Package root, {
required String packageConfigPath,
required String lockFilePath,
}) {
/// Determines if [lockFile] agrees with the given [packagePathsMapping].
///
/// The [packagePathsMapping] is a mapping from package names to paths where
/// the packages are located. (The library is located under
/// `lib/` relative to the path given).
bool isPackagePathsMappingUpToDateWithLockfile(
Map<String, String> packagePathsMapping, {
required String lockFilePath,
required String packageConfigPath,
}) {
// Check that [packagePathsMapping] does not contain more packages than what
// is required. This could lead to import statements working, when they are
// not supposed to work.
final hasExtraMappings = !packagePathsMapping.keys.every((packageName) {
return 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;
});
}
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,
packageConfigPath: packageConfigPath,
lockFilePath: lockFilePath,
)) {
log.fine('The $lockFilePath file has changed since the '
'$packageConfigPath file '
'was generated, please run "$topLevelProgram pub get" again.');
return false;
}
// Check if language version specified in the `package_config.json` is
// correct. This is important for path dependencies as these can mutate.
for (final pkg in packageConfig.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`
/// along with the dir where it resides, if it and `pubspec.lock` exist and
/// are up to date with respect to pubspec.yaml and its dependencies. Or
/// `null` if it is outdated.
///
/// Always returns `null` if `.dart_tool/package_config.json` was generated
/// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
/// different Dart or Flutter SDK version.
///
/// Otherwise first the `modified` timestamps are compared, and if
/// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is
/// newer than all pubspec.yamls of all packages in
/// `.dart_tool/package_config.json` we short-circuit and return true.
///
/// If any of the timestamps are out of order, the resolution in
/// pubspec.lock is validated against constraints of all pubspec.yamls, and
/// the packages of `.dart_tool/package_config.json` is validated against
/// pubspec.lock. We do this extra round of checking to 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 the package configuration and the root dir. Otherwise this
/// returns `null`.
///
/// This check is on the fast-path of `dart run` and should do as little
/// work as possible. Specifically we avoid parsing any yaml when the
/// timestamps are in the right order.
///
/// `.dart_tool/package_config.json` is read 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, String)? isResolutionUpToDate() {
FileStat? packageConfigStat;
late final String packageConfigPath;
late final String rootDir;
for (final parent in parentDirs(dir)) {
final potentialPackageConfigPath =
p.normalize(p.join(parent, '.dart_tool', 'package_config.json'));
packageConfigStat = tryStatFile(potentialPackageConfigPath);
if (packageConfigStat != null) {
packageConfigPath = potentialPackageConfigPath;
rootDir = parent;
break;
}
final potentialPubspacPath = p.join(parent, 'pubspec.yaml');
if (tryStatFile(potentialPubspacPath) == null) {
// No package at [parent] continue to next dir.
continue;
}
final potentialWorkspaceRefPath = p.normalize(
p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
);
final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
if (workspaceRefText == null) {
log.fine(
'`$potentialPubspacPath` exists without corresponding `$potentialPubspacPath` or `$potentialWorkspaceRefPath`.',
);
return null;
} else {
try {
if (jsonDecode(workspaceRefText)
case {'workspaceRoot': final String path}) {
final potentialPackageConfigPath2 = relativeIfNeeded(
p.normalize(
p.absolute(
p.join(
p.dirname(potentialWorkspaceRefPath),
path,
'.dart_tool',
'package_config.json',
),
),
),
);
packageConfigStat = tryStatFile(potentialPackageConfigPath2);
if (packageConfigStat == null) {
log.fine(
'`$potentialWorkspaceRefPath` points to non-existing `$potentialPackageConfigPath2`',
);
return null;
} else {
packageConfigPath = potentialPackageConfigPath2;
rootDir = relativeIfNeeded(
p.normalize(
p.absolute(
p.join(
p.dirname(potentialWorkspaceRefPath),
path,
),
),
),
);
break;
}
} else {
log.fine(
'`$potentialWorkspaceRefPath` is missing "workspaceRoot" property',
);
return null;
}
} on FormatException catch (e) {
log.fine(
'`$potentialWorkspaceRefPath` not valid json: $e.',
);
return null;
}
}
}
if (packageConfigStat == null) {
log.fine(
'Found no .dart_tool/package_config.json - no existing resolution.',
);
return null;
}
final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
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, rootDir);
}
/// 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 flutter = FlutterSdk();
// If Flutter has moved since last invocation, we want to have new
// sdk-packages, and therefore do a new resolution.
//
// This also counts if Flutter was introduced or removed.
final flutterRoot = flutter.rootDirectory == null
? null
: p.toUri(p.absolute(flutter.rootDirectory!)).toString();
if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
log.fine('Flutter has moved since last invocation.');
return null;
}
if (packageConfig.additionalProperties['flutterVersion'] !=
(flutter.isAvailable ? flutter.version.toString() : null)) {
log.fine('Flutter has updated since last invocation.');
return null;
}
// If the pub cache was moved we should have a new resolution.
final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
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(
rootDir,
'.dart_tool',
package.rootUri
// Important to use `toFilePath()` here rather than `path`, as it handles Url-decoding.
.toFilePath(),
'pubspec.yaml',
),
);
if (p.isWithin(cache.rootDir, pubspecPath)) {
continue;
}
final pubspecStat = tryStatFile(pubspecPath);
if (pubspecStat == null) {
log.fine('Could not find `$pubspecPath`');
// A dependency is missing - do a full new resolution.
return null;
}
if (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,
loadPubspec: Pubspec.loadRootWithSources(cache.sources),
);
if (!lockfileNewerThanPubspecs) {
if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
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,
packageConfigPath: packageConfigPath,
lockFilePath: lockFilePath,
)) {
touch(packageConfigPath);
} else {
return null;
}
}
return (packageConfig, rootDir);
}
if (isResolutionUpToDate()
case (final PackageConfig packageConfig, final String rootDir)) {
log.fine('Package Config up to date.');
return (packageConfig: packageConfig, rootDir: rootDir);
}
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 (
packageConfig: entrypoint.packageConfig,
rootDir: relativeIfNeeded(
p.normalize(
p.absolute(entrypoint.workspaceRoot.dir),
),
)
);
}