ensureUpToDate static method

Future<PackageConfig> ensureUpToDate(
  1. String dir, {
  2. required SystemCache cache,
  3. bool summaryOnly = true,
  4. 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.

Implementation

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