ensureUpToDate static method

Future<({PackageConfig packageConfig, String rootDir})> 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.

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