isResolutionUpToDate static method

(PackageConfig, String)? isResolutionUpToDate(
  1. String dir,
  2. SystemCache cache
)

The PackageConfig object representing .dart_tool/package_config.json along with the dir where it resides, if it and pubspec.lock exist and are up to date with respect to pubspec.yaml and its dependencies. Or null if it is outdated.

Always returns null if .dart_tool/package_config.json was generated with a different PUB_CACHE location, a different $FLUTTER_ROOT or a different Dart or Flutter SDK version.

Otherwise first the modified timestamps are compared, and if .dart_tool/package_config.json is newer than pubspec.lock that is newer than all pubspec.yamls of all packages in .dart_tool/package_config.json we short-circuit and return true.

If any of the timestamps are out of order, the resolution in pubspec.lock is validated against constraints of all pubspec.yamls, and the packages of .dart_tool/package_config.json is validated against pubspec.lock. We do this extra round of checking to accommodate for cases where version control or other processes mess up the timestamp order.

If the resolution is still valid, the timestamps are updated and this returns the package configuration and the root dir. Otherwise this returns null.

This check is on the fast-path of dart run and should do as little work as possible. Specifically we avoid parsing any yaml when the timestamps are in the right order.

.dart_tool/package_config.json is read and parsed. In the case of dart run this is acceptable: we speculate that it brings it to the file system cache and the dart VM is going to read the file anyways.

Note this procedure will give false positives if the timestamps are artificially brought in the "right" order. (eg. by manually running touch pubspec.lock; touch .dart_tool/package_config.json) - that is hard to avoid, but also unlikely to happen by accident because .dart_tool/package_config.json is not checked into version control.

Implementation

static (PackageConfig, String)? isResolutionUpToDate(
  String dir,
  SystemCache cache,
) {
  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.packages;
    for (final pkg in packagesToCheck) {
      // Pub always makes a packageUri of lib/
      if (pkg.packageUri == null || pkg.packageUri.toString() != 'lib/') {
        log.fine(
          'The "$packageConfigPath" file '
          'is not recognized by this pub version.',
        );
        return false;
      }
      packagePathsMapping[pkg.name] = 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.packages) {
      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;
  }

  FileStat? packageConfigStat;
  late final String packageConfigPath;
  late final String rootDir;
  for (final parent in parentDirs(dir)) {
    final potentialPackageConfigPath = p.normalize(
      p.join(parent, '.dart_tool', 'package_config.json'),
    );
    packageConfigStat = tryStatFile(potentialPackageConfigPath);

    if (packageConfigStat != null) {
      packageConfigPath = potentialPackageConfigPath;
      rootDir = parent;
      break;
    }
    final potentialPubspecPath = p.join(parent, 'pubspec.yaml');
    if (tryStatFile(potentialPubspecPath) == null) {
      // No package at [parent] continue to next dir.
      continue;
    }

    final potentialWorkspaceRefPath = p.normalize(
      p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
    );

    final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
    if (workspaceRefText == null) {
      log.fine(
        '`$potentialPubspecPath` exists without corresponding '
        '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.',
      );
      return null;
    } else {
      try {
        if (jsonDecode(workspaceRefText) case {
          'workspaceRoot': final String path,
        }) {
          final potentialPackageConfigPath2 = relativeIfNeeded(
            p.normalize(
              p.absolute(
                p.join(
                  p.dirname(potentialWorkspaceRefPath),
                  path,
                  '.dart_tool',
                  'package_config.json',
                ),
              ),
            ),
          );
          packageConfigStat = tryStatFile(potentialPackageConfigPath2);
          if (packageConfigStat == null) {
            log.fine(
              '`$potentialWorkspaceRefPath` points to non-existing '
              '`$potentialPackageConfigPath2`',
            );
            return null;
          } else {
            packageConfigPath = potentialPackageConfigPath2;
            rootDir = relativeIfNeeded(
              p.normalize(
                p.absolute(
                  p.join(p.dirname(potentialWorkspaceRefPath), path),
                ),
              ),
            );

            break;
          }
        } else {
          log.fine(
            '`$potentialWorkspaceRefPath` '
            'is missing "workspaceRoot" property',
          );
          return null;
        }
      } on FormatException catch (e) {
        log.fine('`$potentialWorkspaceRefPath` not valid json: $e.');
        return null;
      }
    }
  }
  if (packageConfigStat == null) {
    log.fine(
      'Found no .dart_tool/package_config.json - no existing resolution.',
    );
    return null;
  }
  final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
  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 was
  /// generated by a different sdk down to changes in minor versions.
  bool isPackageConfigGeneratedBySameDartSdk() {
    final generatorVersion = packageConfig.generatorVersion;
    if (generatorVersion == null ||
        generatorVersion.major != sdk.version.major ||
        generatorVersion.minor != sdk.version.minor) {
      log.fine('The Dart SDK was updated since last package resolution.');
      return false;
    }
    return true;
  }

  final flutter = FlutterSdk();
  // If Flutter has moved since last invocation, we want to have new
  // sdk-packages, and therefore do a new resolution.
  //
  // This also counts if Flutter was introduced or removed.
  final flutterRoot =
      flutter.rootDirectory == null
          ? null
          : p.toUri(p.absolute(flutter.rootDirectory!)).toString();
  if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
    log.fine('Flutter has moved since last invocation.');
    return null;
  }
  if (packageConfig.additionalProperties['flutterVersion'] !=
      (flutter.isAvailable ? flutter.version.toString() : null)) {
    log.fine('Flutter has updated since last invocation.');
    return null;
  }
  // If the pub cache was moved we should have a new resolution.
  final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
  if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
    final previousPubCachePath =
        packageConfig.additionalProperties['pubCache'];
    log.fine(
      'The pub cache has moved from $previousPubCachePath to $rootCacheUrl '
      'since last invocation.',
    );
    return null;
  }
  // If the Dart sdk was updated we want a new resolution.
  if (!isPackageConfigGeneratedBySameDartSdk()) {
    return null;
  }
  final lockFileStat = tryStatFile(lockFilePath);
  if (lockFileStat == null) {
    log.fine('No $lockFilePath file found.');
    return null;
  }

  final lockFileModified = lockFileStat.modified;
  var lockfileNewerThanPubspecs = true;

  // 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);
}