Lazy loading of pubspec.yaml from entrypoint (#3835)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index c6e7c84..eeae0c7 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -11,7 +11,6 @@
 
 import '../command.dart';
 import '../command_runner.dart';
-import '../entrypoint.dart';
 import '../exceptions.dart';
 import '../git.dart';
 import '../io.dart';
@@ -184,10 +183,13 @@
     final updates =
         argResults.rest.map((p) => _parsePackage(p, argResults)).toList();
 
-    var updatedPubSpec = entrypoint.root.pubspec;
+    /// Compute a pubspec that will depend on all the given packages, but the
+    /// actual constraint will only be determined after a resolution decides the
+    /// best version.
+    var resolutionPubspec = entrypoint.root.pubspec;
     for (final update in updates) {
       /// Perform version resolution in-memory.
-      updatedPubSpec = await _addPackageToPubspec(updatedPubSpec, update);
+      resolutionPubspec = await _addPackageToPubspec(resolutionPubspec, update);
     }
 
     late SolveResult solveResult;
@@ -201,7 +203,7 @@
       solveResult = await resolveVersions(
         SolveType.upgrade,
         cache,
-        Package.inMemory(updatedPubSpec),
+        Package.inMemory(resolutionPubspec),
       );
     } on GitException {
       final name = updates.first.ref.name;
@@ -224,7 +226,7 @@
       /// Assert that [resultPackage] is within the original user's expectations.
       final constraint = update.constraint;
       if (constraint != null && !constraint.allows(resultPackage.version)) {
-        final dependencyOverrides = updatedPubSpec.dependencyOverrides;
+        final dependencyOverrides = resolutionPubspec.dependencyOverrides;
         if (dependencyOverrides.isNotEmpty) {
           dataError('"$name" resolved to "${resultPackage.version}" which '
               'does not satisfy constraint "$constraint". This could be '
@@ -232,50 +234,42 @@
         }
       }
     }
-    if (argResults.isDryRun) {
-      /// Even if it is a dry run, run `acquireDependencies` so that the user
-      /// gets a report on the other packages that might change version due
-      /// to this new dependency.
-      final newRoot = Package.inMemory(updatedPubSpec);
-
-      await Entrypoint.inMemory(
-        newRoot,
-        cache,
-        solveResult: solveResult,
-        lockFile: entrypoint.lockFile,
-      ).acquireDependencies(
-        SolveType.get,
-        dryRun: true,
-        precompile: argResults.shouldPrecompile,
-        analytics: analytics,
-      );
-    } else {
+    final newPubspecText = _updatePubspec(solveResult.packages, updates);
+    if (!argResults.isDryRun) {
       /// Update the `pubspec.yaml` before calling [acquireDependencies] to
       /// ensure that the modification timestamp on `pubspec.lock` and
       /// `.dart_tool/package_config.json` is newer than `pubspec.yaml`,
       /// ensuring that [entrypoint.assertUptoDate] will pass.
-      _updatePubspec(
-        solveResult.packages,
-        updates,
-      );
+      writeTextFile(entrypoint.pubspecPath, newPubspecText);
+    }
 
-      /// Create a new [Entrypoint] since we have to reprocess the updated
-      /// pubspec file.
-      final updatedEntrypoint = Entrypoint(directory, cache);
-      await updatedEntrypoint.acquireDependencies(
+    /// Even if it is a dry run, run `acquireDependencies` so that the user
+    /// gets a report on the other packages that might change version due
+    /// to this new dependency.
+    await entrypoint
+        .withPubspec(
+          Pubspec.parse(
+            newPubspecText,
+            cache.sources,
+            location: Uri.parse(entrypoint.pubspecPath),
+          ),
+        )
+        .acquireDependencies(
+          SolveType.get,
+          dryRun: argResults.isDryRun,
+          precompile: !argResults.isDryRun && argResults.shouldPrecompile,
+          analytics: argResults.isDryRun ? null : analytics,
+        );
+
+    if (!argResults.isDryRun &&
+        argResults.example &&
+        entrypoint.example != null) {
+      await entrypoint.example!.acquireDependencies(
         SolveType.get,
         precompile: argResults.shouldPrecompile,
+        summaryOnly: true,
         analytics: analytics,
       );
-
-      if (argResults.example && entrypoint.example != null) {
-        await entrypoint.example!.acquireDependencies(
-          SolveType.get,
-          precompile: argResults.shouldPrecompile,
-          summaryOnly: true,
-          analytics: analytics,
-        );
-      }
     }
 
     if (isOffline) {
@@ -656,8 +650,8 @@
     );
   }
 
-  /// Writes the changes to the pubspec file.
-  void _updatePubspec(
+  /// Calculates the updates to the pubspec file.
+  String _updatePubspec(
     List<PackageId> resultPackages,
     List<_ParseResult> updates,
   ) {
@@ -683,7 +677,7 @@
       } else {
         pubspecInformation = {
           ref.source.name: ref.description.serializeForPubspec(
-            containingDir: entrypoint.root.dir,
+            containingDir: entrypoint.rootDir,
             languageVersion: entrypoint.root.pubspec.languageVersion,
           ),
           if (description is HostedDescription || constraint != null)
@@ -730,8 +724,7 @@
       }
     }
 
-    /// Windows line endings are already handled by [yamlEditor]
-    writeTextFile(entrypoint.pubspecPath, yamlEditor.toString());
+    return yamlEditor.toString();
   }
 }
 
diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart
index fff66ab..b34e33e 100644
--- a/lib/src/command/deps.dart
+++ b/lib/src/command/deps.dart
@@ -10,6 +10,7 @@
 import '../command_runner.dart';
 import '../log.dart' as log;
 import '../package.dart';
+import '../pubspec.dart';
 import '../sdk.dart';
 import '../utils.dart';
 
@@ -101,7 +102,7 @@
                 : currentPackage.dependencies)
             .keys
             .toList();
-        final dependencyType = entrypoint.root.dependencyType(current);
+        final dependencyType = entrypoint.root.pubspec.dependencyType(current);
         final kind = currentPackage == entrypoint.root
             ? 'root'
             : (dependencyType == DependencyType.direct
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index b5330c9..89db747 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -260,11 +260,11 @@
     var package = entrypoint.root;
     log.message(
       'Publishing ${package.name} ${package.version} to $host:\n'
-      '${tree.fromFiles(files, baseDir: entrypoint.root.dir, showFileSizes: true)}',
+      '${tree.fromFiles(files, baseDir: entrypoint.rootDir, showFileSizes: true)}',
     );
 
     var packageBytesFuture =
-        createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
+        createTarGz(files, baseDir: entrypoint.rootDir).toBytes();
 
     // Validate the package.
     var isValid = await _validate(
diff --git a/lib/src/command/list_package_dirs.dart b/lib/src/command/list_package_dirs.dart
new file mode 100644
index 0000000..b572a9e
--- /dev/null
+++ b/lib/src/command/list_package_dirs.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2013, 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 'package:path/path.dart' as p;
+
+import '../command.dart';
+import '../command_runner.dart';
+import '../io.dart';
+import '../log.dart' as log;
+import '../package_name.dart';
+import '../utils.dart';
+
+/// Handles the `list-package-dirs` pub command.
+class ListPackageDirsCommand extends PubCommand {
+  @override
+  String get name => 'list-package-dirs';
+  @override
+  String get description => 'Print local paths to dependencies.';
+  @override
+  String get argumentsDescription => '';
+  @override
+  bool get takesArguments => false;
+  @override
+  bool get hidden => true;
+
+  ListPackageDirsCommand() {
+    argParser.addOption(
+      'format',
+      help: 'How output should be displayed.',
+      allowed: ['json'],
+    );
+    argParser.addOption(
+      'directory',
+      abbr: 'C',
+      help: 'Run this in the directory <dir>.',
+      valueHelp: 'dir',
+    );
+  }
+
+  @override
+  Future<void> runProtected() async {
+    log.json.enabled = true;
+    entrypoint.assertUpToDate();
+    if (!fileExists(entrypoint.lockFilePath)) {
+      dataError(
+        'Package "myapp" has no lockfile. Please run "$topLevelProgram pub get" first.',
+      );
+    }
+
+    var output = {};
+
+    // Include the local paths to all locked packages.
+    var packages = mapMap(
+      entrypoint.lockFile.packages,
+      value: (String name, PackageId package) {
+        var packageDir = cache.getDirectory(package);
+        // Normalize paths and make them absolute for backwards compatibility
+        // with the protocol used by the analyzer.
+        return p.normalize(p.absolute(p.join(packageDir, 'lib')));
+      },
+    );
+
+    // Include the self link.
+    packages[entrypoint.root.name] =
+        p.normalize(p.absolute(entrypoint.root.path('lib')));
+
+    output['packages'] = packages;
+
+    // Include the file(s) which when modified will affect the results. For pub,
+    // that's just the pubspec and lockfile.
+    output['input_files'] = [
+      p.normalize(p.absolute(entrypoint.lockFilePath)),
+      p.normalize(p.absolute(entrypoint.pubspecPath))
+    ];
+
+    log.json.message(output);
+  }
+}
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index cf65241..c993cd0 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -6,10 +6,8 @@
 import 'package:yaml_edit/yaml_edit.dart';
 
 import '../command.dart';
-import '../entrypoint.dart';
 import '../io.dart';
 import '../log.dart' as log;
-import '../package.dart';
 import '../pubspec.dart';
 import '../solver.dart';
 
@@ -84,40 +82,28 @@
       return _PackageRemoval(name, removeFromOverride: isOverride);
     });
 
-    if (isDryRun) {
-      final rootPubspec = entrypoint.root.pubspec;
-      final newPubspec = _removePackagesFromPubspec(rootPubspec, targets);
-      final newRoot = Package.inMemory(newPubspec);
-
-      await Entrypoint.inMemory(newRoot, cache, lockFile: entrypoint.lockFile)
-          .acquireDependencies(
-        SolveType.get,
-        precompile: argResults['precompile'],
-        dryRun: true,
-        analytics: null,
-      );
-    } else {
+    if (!isDryRun) {
       /// Update the pubspec.
       _writeRemovalToPubspec(targets);
+    }
+    final rootPubspec = entrypoint.root.pubspec;
+    final newPubspec = _removePackagesFromPubspec(rootPubspec, targets);
 
-      /// Create a new [Entrypoint] since we have to reprocess the updated
-      /// pubspec file.
-      final updatedEntrypoint = Entrypoint(directory, cache);
-      await updatedEntrypoint.acquireDependencies(
+    await entrypoint.withPubspec(newPubspec).acquireDependencies(
+          SolveType.get,
+          precompile: !isDryRun && argResults['precompile'],
+          dryRun: isDryRun,
+          analytics: isDryRun ? null : analytics,
+        );
+
+    var example = entrypoint.example;
+    if (!isDryRun && argResults['example'] && example != null) {
+      await example.acquireDependencies(
         SolveType.get,
         precompile: argResults['precompile'],
+        summaryOnly: true,
         analytics: analytics,
       );
-
-      var example = entrypoint.example;
-      if (argResults['example'] && example != null) {
-        await example.acquireDependencies(
-          SolveType.get,
-          precompile: argResults['precompile'],
-          summaryOnly: true,
-          analytics: analytics,
-        );
-      }
     }
   }
 
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 17db3e7..7efa66e 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -113,7 +113,7 @@
     if (_upgradeMajorVersions) {
       if (argResults['example'] && entrypoint.example != null) {
         log.warning(
-          'Running `upgrade --major-versions` only in `${entrypoint.root.dir}`. Run `$topLevelProgram pub upgrade --major-versions --directory example/` separately.',
+          'Running `upgrade --major-versions` only in `${entrypoint.rootDir}`. Run `$topLevelProgram pub upgrade --major-versions --directory example/` separately.',
         );
       }
       await _runUpgradeMajorVersions();
@@ -240,36 +240,27 @@
     final solveType =
         argResults.rest.isEmpty ? SolveType.upgrade : SolveType.get;
 
-    if (_dryRun) {
-      // Even if it is a dry run, run `acquireDependencies` so that the user
-      // gets a report on changes.
-      await Entrypoint.inMemory(
-        Package.inMemory(
-          Pubspec.parse(newPubspecText, cache.sources),
-        ),
-        cache,
-        lockFile: entrypoint.lockFile,
-        solveResult: solveResult,
-      ).acquireDependencies(
-        solveType,
-        dryRun: true,
-        precompile: _precompile,
-        analytics: null, // No analytics for dry-run
-      );
-    } else {
+    if (!_dryRun) {
       if (changes.isNotEmpty) {
         writeTextFile(entrypoint.pubspecPath, newPubspecText);
       }
-      // TODO: Allow Entrypoint to be created with in-memory pubspec, so that
-      //       we can show the changes when not in --dry-run mode. For now we only show
-      //       the changes made to pubspec.yaml in dry-run mode.
-      await Entrypoint(directory, cache).acquireDependencies(
-        solveType,
-        precompile: _precompile,
-        analytics: analytics,
-      );
     }
 
+    await entrypoint
+        .withPubspec(
+          Pubspec.parse(
+            newPubspecText,
+            cache.sources,
+            location: Uri.parse(entrypoint.pubspecPath),
+          ),
+        )
+        .acquireDependencies(
+          solveType,
+          dryRun: _dryRun,
+          precompile: !_dryRun && _precompile,
+          analytics: _dryRun ? null : analytics, // No analytics for dry-run
+        );
+
     _outputChangeSummary(changes);
 
     // If any of the packages to upgrade are dependency overrides, then we
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 1b98da6..9a01f9b 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -73,10 +73,24 @@
 /// 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 directory where the package is stored.
+  ///
+  /// For global packages this is inside the pub cache.
+  ///
+  /// Except for packages globally activated from path.
+  final String rootDir;
+
+  Package? _root;
+
   /// The root package this entrypoint is associated with.
   ///
   /// For a global package, this is the activated package.
-  final Package root;
+  Package get root => _root ??= Package.load(
+        null,
+        rootDir,
+        cache.sources,
+        withPubspecOverrides: true,
+      );
 
   /// For a global package, this is the directory that the package is installed
   /// in. Non-global packages have null.
@@ -87,7 +101,7 @@
   final SystemCache cache;
 
   /// Whether this entrypoint exists within the package cache.
-  bool get isCached => !root.isInMemory && p.isWithin(cache.rootDir, root.dir);
+  bool get isCached => p.isWithin(cache.rootDir, rootDir);
 
   /// Whether this is an entrypoint for a globally-activated package.
   // final bool isGlobal;
@@ -175,7 +189,7 @@
   ///
   /// Global packages (except those from path source)
   /// store these in the global cache.
-  String? get _configRoot => isCached ? globalDir : root.dir;
+  String? get _configRoot => isCached ? globalDir : rootDir;
 
   /// The path to the entrypoint's ".packages" file.
   ///
@@ -191,15 +205,11 @@
   );
 
   /// 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);
+  String get pubspecPath => p.normalize(p.join(rootDir, 'pubspec.yaml'));
 
   /// The path to the entrypoint package's pubspec overrides file.
   String get pubspecOverridesPath =>
-      p.normalize(root.path('pubspec_overrides.yaml'));
+      p.normalize(p.join(rootDir, 'pubspec_overrides.yaml'));
 
   /// The path to the entrypoint package's lockfile.
   String get lockFilePath => p.normalize(p.join(_configRoot!, 'pubspec.lock'));
@@ -209,7 +219,7 @@
   /// 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 => globalDir ?? root.path('.dart_tool/pub');
+  String get cachePath => globalDir ?? p.join(rootDir, '.dart_tool/pub');
 
   /// The path to the directory containing dependency executable snapshots.
   String get _snapshotPath => p.join(cachePath, 'bin');
@@ -218,44 +228,51 @@
   /// builds.
   String get _incrementalDillsPath => p.join(cachePath, 'incremental');
 
-  /// Loads the entrypoint from a package at [rootDir].
+  Entrypoint._(
+    this.rootDir,
+    this._lockFile,
+    this._example,
+    this._packageGraph,
+    this.cache,
+    this._root,
+    this.globalDir,
+  );
+
+  /// An entrypoint representing a package at [rootDir].
   Entrypoint(
-    String rootDir,
+    this.rootDir,
     this.cache, {
-    bool withPubspecOverrides = true,
-  })  : root = Package.load(
-          null,
-          rootDir,
-          cache.sources,
-          withPubspecOverrides: withPubspecOverrides,
-        ),
+    Pubspec? pubspec,
+  })  : _root = pubspec == null ? null : Package.inMemory(pubspec),
         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 at the same location, that will use [pubspec] for
+  /// resolution.
+  Entrypoint withPubspec(Pubspec pubspec) {
+    return Entrypoint._(
+      rootDir,
+      _lockFile,
+      _example,
+      _packageGraph,
+      cache,
+      Package.inMemory(pubspec),
+      globalDir,
+    );
   }
 
   /// 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,
+    Package this._root,
     this._lockFile,
     this.cache, {
     SolveResult? solveResult,
-  }) {
+  }) : rootDir = _root.dir {
     if (solveResult != null) {
       _packageGraph = PackageGraph.fromSolveResult(this, solveResult);
     }
@@ -285,7 +302,7 @@
         entrypoint: entrypointName,
         entrypointSdkConstraint:
             root.pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
-        relativeFrom: isGlobal ? null : root.dir,
+        relativeFrom: isGlobal ? null : rootDir,
       ),
     );
   }
@@ -325,8 +342,9 @@
     bool summaryOnly = false,
     bool enforceLockfile = false,
   }) async {
+    root; // This will throw early if pubspec.yaml could not be found.
     summaryOnly = summaryOnly || _summaryOnlyEnvironment;
-    final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
+    final suffix = rootDir == '.' ? '' : ' in $rootDir';
 
     if (enforceLockfile && !fileExists(lockFilePath)) {
       throw ApplicationException('''
@@ -354,7 +372,8 @@
 
     final report = SolveReport(
       type,
-      root,
+      rootDir,
+      root.pubspec,
       lockFile,
       newLockFile,
       result.availableVersions,
@@ -597,13 +616,22 @@
   /// the resolution automatically.
   void assertUpToDate({bool checkForSdkUpdate = false}) {
     if (isCached) return;
-
-    if (!entryExists(lockFilePath)) {
+    final pubspecStat = tryStatFile(pubspecPath);
+    if (pubspecStat == null) {
+      throw FileException(
+        'Could not find a file named "pubspec.yaml" in '
+        '"${canonicalize(rootDir)}".',
+        pubspecPath,
+      );
+    }
+    final lockFileStat = tryStatFile(lockFilePath);
+    if (lockFileStat == null) {
       dataError(
         'No $lockFilePath file found, please run "$topLevelProgram pub get" first.',
       );
     }
-    if (!entryExists(packageConfigPath)) {
+    final packageConfigStat = tryStatFile(packageConfigPath);
+    if (packageConfigStat == null) {
       dataError(
         'No $packageConfigPath file found, please run "$topLevelProgram pub get".\n'
         '\n'
@@ -617,17 +645,15 @@
     var lockFileText = readTextFile(lockFilePath);
     var hasPathDependencies = lockFileText.contains('\n    source: path\n');
 
-    var pubspecModified = File(pubspecPath).lastModifiedSync();
-    var lockFileModified = File(lockFilePath).lastModifiedSync();
+    var lockFileModified = lockFileStat.modified;
 
-    var pubspecChanged = lockFileModified.isBefore(pubspecModified);
+    var pubspecChanged = lockFileModified.isBefore(pubspecStat.modified);
     var pubspecOverridesChanged = false;
 
-    if (hasPubspecOverrides) {
-      var pubspecOverridesModified =
-          File(pubspecOverridesPath).lastModifiedSync();
+    final pubspecOverridesStat = tryStatFile(pubspecOverridesPath);
+    if (pubspecOverridesStat != null) {
       pubspecOverridesChanged =
-          lockFileModified.isBefore(pubspecOverridesModified);
+          lockFileModified.isBefore(pubspecOverridesStat.modified);
     }
 
     var touchedLockFile = false;
@@ -650,8 +676,7 @@
       }
     }
 
-    var packageConfigModified = File(packageConfigPath).lastModifiedSync();
-    if (packageConfigModified.isBefore(lockFileModified) ||
+    if (packageConfigStat.modified.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
@@ -666,9 +691,9 @@
       }
     }
 
-    for (var match in _sdkConstraint.allMatches(lockFileText)) {
-      var identifier = match[1] == 'sdk' ? 'dart' : match[1]!.trim();
-      var sdk = sdks[identifier]!;
+    for (final match in _sdkConstraint.allMatches(lockFileText)) {
+      final identifier = match[1] == 'sdk' ? 'dart' : match[1]!.trim();
+      final 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
@@ -676,7 +701,7 @@
       // able to `pub run` non-Flutter tools even in a Flutter app.
       if (!sdk.isAvailable) continue;
 
-      var parsedConstraint = VersionConstraint.parse(match[2]!);
+      final 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.");
@@ -792,7 +817,7 @@
 
       final source = lockFileId.source;
       final lockFilePackagePath = root.path(
-        cache.getDirectory(lockFileId, relativeFrom: root.dir),
+        cache.getDirectory(lockFileId, relativeFrom: rootDir),
       );
 
       // Make sure that the packagePath agrees with the lock file about the
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 1d35144..eaa068c 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -178,7 +178,7 @@
     _describeActive(name, cache);
 
     // Write a lockfile that points to the local package.
-    var fullPath = canonicalize(entrypoint.root.dir);
+    var fullPath = canonicalize(entrypoint.rootDir);
     var id = cache.path.idFor(
       name,
       entrypoint.root.version,
@@ -261,7 +261,8 @@
       if (!silent) {
         await SolveReport(
           SolveType.get,
-          root,
+          null,
+          root.pubspec,
           originalLockFile ?? LockFile.empty(),
           lockFile,
           result.availableVersions,
@@ -574,7 +575,7 @@
             );
           } else {
             await activatePath(
-              entrypoint.root.dir,
+              entrypoint.rootDir,
               packageExecutables,
               overwriteBinStubs: true,
               analytics: null,
diff --git a/lib/src/http.dart b/lib/src/http.dart
index 00f5a7f..ab302fd 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -13,7 +13,7 @@
 
 import 'command.dart';
 import 'log.dart' as log;
-import 'package.dart';
+import 'pubspec.dart';
 import 'sdk.dart';
 import 'source/hosted.dart';
 import 'utils.dart';
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 005f6c5..8f510f6 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -81,6 +81,20 @@
 /// points to a file.
 bool fileExists(String file) => File(file).existsSync();
 
+/// Stats [path], assuming it or the entry it is a link to is a file.
+///
+/// Returns `null` if it is not a file (eg. a directory or not existing).
+FileStat? tryStatFile(String path) {
+  var stat = File(path).statSync();
+  if (stat.type == FileSystemEntityType.link) {
+    stat = File(File(path).resolveSymbolicLinksSync()).statSync();
+  }
+  if (stat.type == FileSystemEntityType.file) {
+    return stat;
+  }
+  return null;
+}
+
 /// Returns the canonical path for [pathString].
 ///
 /// This is the normalized, absolute path, with symlinks resolved. As in
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 6aeeb78..9853418 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -181,17 +181,6 @@
     return p.relative(path, from: dir);
   }
 
-  /// Returns the type of dependency from this package onto [name].
-  DependencyType dependencyType(String? name) {
-    if (pubspec.fields['dependencies']?.containsKey(name) ?? false) {
-      return DependencyType.direct;
-    } else if (pubspec.fields['dev_dependencies']?.containsKey(name) ?? false) {
-      return DependencyType.dev;
-    } else {
-      return DependencyType.none;
-    }
-  }
-
   static final _basicIgnoreRules = [
     '.*', // Don't include dot-files.
     '!.htaccess', // Include .htaccess anyways.
@@ -329,22 +318,3 @@
     ).map(resolve).toList();
   }
 }
-
-/// The type of dependency from one package to another.
-class DependencyType {
-  /// A dependency declared in `dependencies`.
-  static const direct = DependencyType._('direct');
-
-  /// A dependency declared in `dev_dependencies`.
-  static const dev = DependencyType._('dev');
-
-  /// No dependency exists.
-  static const none = DependencyType._('none');
-
-  final String _name;
-
-  const DependencyType._(this._name);
-
-  @override
-  String toString() => _name;
-}
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index a1f84c1..5f27abc 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -433,6 +433,27 @@
     collectError(() => sdkConstraints);
     return errors;
   }
+
+  /// Returns the type of dependency from this package onto [name].
+  DependencyType dependencyType(String? name) {
+    if (dependencies.containsKey(name)) {
+      return DependencyType.direct;
+    } else if (devDependencies.containsKey(name)) {
+      return DependencyType.dev;
+    } else {
+      return DependencyType.none;
+    }
+  }
+}
+
+/// The type of dependency from one package to another.
+enum DependencyType {
+  direct,
+  dev,
+  none;
+
+  @override
+  String toString() => name;
 }
 
 /// Parses the dependency field named [field], and returns the corresponding
diff --git a/lib/src/solver/report.dart b/lib/src/solver/report.dart
index 1f57423..bbc4381 100644
--- a/lib/src/solver/report.dart
+++ b/lib/src/solver/report.dart
@@ -8,7 +8,6 @@
 import '../command_runner.dart';
 import '../lock_file.dart';
 import '../log.dart' as log;
-import '../package.dart';
 import '../package_name.dart';
 import '../pubspec.dart';
 import '../source/hosted.dart';
@@ -25,7 +24,9 @@
 /// It's a report builder.
 class SolveReport {
   final SolveType _type;
-  final Package _root;
+  // The report will contain "in [_location]" if given.
+  final String? _location;
+  final Pubspec _rootPubspec;
   final LockFile _previousLockFile;
   final LockFile _newLockFile;
   final SystemCache _cache;
@@ -46,7 +47,8 @@
 
   SolveReport(
     this._type,
-    this._root,
+    this._location,
+    this._rootPubspec,
     this._previousLockFile,
     this._newLockFile,
     this._availableVersions,
@@ -141,7 +143,7 @@
     // Count how many dependencies actually changed.
     var dependencies = _newLockFile.packages.keys.toSet();
     dependencies.addAll(_previousLockFile.packages.keys);
-    dependencies.remove(_root.name);
+    dependencies.remove(_rootPubspec.name);
 
     var numChanged = dependencies.where((name) {
       var oldId = _previousLockFile.packages[name];
@@ -156,8 +158,8 @@
     }).length;
 
     var suffix = '';
-    if (!_root.isInMemory) {
-      final dir = _root.dir;
+    final dir = _location;
+    if (dir != null) {
       if (dir != '.') {
         suffix = ' in $dir';
       }
@@ -218,7 +220,7 @@
     final output = StringBuffer();
     // Show the new set of dependencies ordered by name.
     var names = _newLockFile.packages.keys.toList();
-    names.remove(_root.name);
+    names.remove(_rootPubspec.name);
     names.sort();
     var hasChanges = false;
     for (final name in names) {
@@ -227,7 +229,7 @@
     // Show any removed ones.
     var removed = _previousLockFile.packages.keys.toSet();
     removed.removeAll(names);
-    removed.remove(_root.name); // Never consider root.
+    removed.remove(_rootPubspec.name); // Never consider root.
     if (removed.isNotEmpty) {
       output.writeln('These packages are no longer being depended on:');
       for (var name in ordered(removed)) {
@@ -249,8 +251,8 @@
       final status = await id.source
           .status(id.toRef(), id.version, _cache, maxAge: Duration(days: 3));
       if (status.isDiscontinued &&
-          (_root.dependencyType(id.name) == DependencyType.direct ||
-              _root.dependencyType(id.name) == DependencyType.dev)) {
+          (_rootPubspec.dependencyType(id.name) == DependencyType.direct ||
+              _rootPubspec.dependencyType(id.name) == DependencyType.dev)) {
         numDiscontinued++;
       }
     }
@@ -306,7 +308,7 @@
     var oldId = _previousLockFile.packages[name];
     var id = newId ?? oldId!;
 
-    var isOverridden = _root.dependencyOverrides.containsKey(id.name);
+    var isOverridden = _rootPubspec.dependencyOverrides.containsKey(id.name);
 
     // If the package was previously a dependency but the dependency has
     // changed in some way.
@@ -386,8 +388,8 @@
           message = '(retracted)';
         }
       } else if (status.isDiscontinued &&
-          (_root.dependencyType(name) == DependencyType.direct ||
-              _root.dependencyType(name) == DependencyType.dev)) {
+          [DependencyType.direct, DependencyType.dev]
+              .contains(_rootPubspec.dependencyType(name))) {
         if (status.discontinuedReplacedBy == null) {
           message = '(discontinued)';
         } else {
@@ -428,10 +430,12 @@
 
     // Highlight overridden packages.
     if (isOverridden) {
-      final location = _root.pubspec.dependencyOverridesFromOverridesFile
-          ? ' in ${p.join(_root.dir, Pubspec.pubspecOverridesFilename)}'
-          : '';
-      output.write(' ${log.magenta('(overridden$location)')}');
+      final location = _location;
+      final overrideLocation =
+          location != null && _rootPubspec.dependencyOverridesFromOverridesFile
+              ? ' in ${p.join(location, Pubspec.pubspecOverridesFilename)}'
+              : '';
+      output.write(' ${log.magenta('(overridden$overrideLocation)')}');
     }
 
     if (message != null) output.write(' ${log.cyan(message)}');
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index 2be099e..bb70ef1 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -65,7 +65,7 @@
     final resolvedPackageIds = await Future.wait(
       packages.map((id) async {
         if (id.source is CachedSource) {
-          return await withDependencyType(_root.dependencyType(id.name),
+          return await withDependencyType(_root.pubspec.dependencyType(id.name),
               () async {
             return await cache.downloadPackage(
               id,
@@ -153,7 +153,7 @@
         DependencyType.dev: 'dev',
         DependencyType.direct: 'direct',
         DependencyType.none: 'transitive'
-      }[_root.dependencyType(package.name)]!;
+      }[_root.pubspec.dependencyType(package.name)]!;
       analytics.sendEvent(
         'pub-get',
         package.name,
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index 1b341d4..e8a2346 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -512,7 +512,7 @@
         _systemCache,
         ref,
         locked,
-        _root.dependencyType(package.name),
+        _root.pubspec.dependencyType(package.name),
         overridden,
         _getAllowedRetracted(ref.name),
         downgrade: _type == SolveType.downgrade,
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 6fcab2a..07ef9b3 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -215,7 +215,7 @@
   /// entrypoint).
   // TODO(sigurdm): Consider moving this to a more central location.
   List<String> filesBeneath(String dir, {required bool recursive}) {
-    final base = p.canonicalize(p.join(entrypoint.root.dir, dir));
+    final base = p.canonicalize(p.join(entrypoint.rootDir, dir));
     return files
         .where(
           recursive
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart
index b1e70a2..d22da8d 100644
--- a/lib/src/validator/analyze.dart
+++ b/lib/src/validator/analyze.dart
@@ -18,7 +18,7 @@
   @override
   Future<void> validate() async {
     final dirsToAnalyze = ['lib', 'test', 'bin']
-        .map((dir) => p.join(entrypoint.root.dir, dir))
+        .map((dir) => p.join(entrypoint.rootDir, dir))
         .where(dirExists);
     final result = await runProcess(
       Platform.resolvedExecutable,
@@ -26,7 +26,7 @@
         'analyze',
         '--fatal-infos',
         ...dirsToAnalyze,
-        p.join(entrypoint.root.dir, 'pubspec.yaml')
+        p.join(entrypoint.rootDir, 'pubspec.yaml')
       ],
     );
     if (result.exitCode != 0) {
diff --git a/lib/src/validator/directory.dart b/lib/src/validator/directory.dart
index 8a3e7f2..a7ef221 100644
--- a/lib/src/validator/directory.dart
+++ b/lib/src/validator/directory.dart
@@ -27,8 +27,8 @@
     for (final file in files) {
       // Find the topmost directory name of [file].
       final dir = path.join(
-        entrypoint.root.dir,
-        path.split(path.relative(file, from: entrypoint.root.dir)).first,
+        entrypoint.rootDir,
+        path.split(path.relative(file, from: entrypoint.rootDir)).first,
       );
       if (!visited.add(dir)) continue;
       if (!dirExists(dir)) continue;
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index 9a1770a..e30bb01 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -30,7 +30,7 @@
             '--exclude-standard',
             '--recurse-submodules'
           ],
-          workingDir: entrypoint.root.dir,
+          workingDir: entrypoint.rootDir,
         );
       } on git.GitException catch (e) {
         log.fine('Could not run `git ls-files` files in repo (${e.message}).');
@@ -39,9 +39,9 @@
         // --recurse-submodules we just continue silently.
         return;
       }
-      final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir;
+      final root = git.repoRoot(entrypoint.rootDir) ?? entrypoint.rootDir;
       var beneath = p.posix.joinAll(
-        p.split(p.normalize(p.relative(entrypoint.root.dir, from: root))),
+        p.split(p.normalize(p.relative(entrypoint.rootDir, from: root))),
       );
       if (beneath == './') {
         beneath = '';
@@ -73,7 +73,7 @@
         },
         isDir: (dir) => dirExists(resolve(dir)),
       ).map((file) {
-        final relative = p.relative(resolve(file), from: entrypoint.root.dir);
+        final relative = p.relative(resolve(file), from: entrypoint.rootDir);
         return Platform.isWindows
             ? p.posix.joinAll(p.split(relative))
             : relative;
diff --git a/lib/src/validator/strict_dependencies.dart b/lib/src/validator/strict_dependencies.dart
index 2fec2f6..728f592 100644
--- a/lib/src/validator/strict_dependencies.dart
+++ b/lib/src/validator/strict_dependencies.dart
@@ -23,7 +23,7 @@
   /// Files that do not parse and directives that don't import or export
   /// `package:` URLs are ignored.
   Iterable<_Usage> _findPackages(Iterable<String> files) sync* {
-    final packagePath = p.normalize(p.absolute(entrypoint.root.dir));
+    final packagePath = p.normalize(p.absolute(entrypoint.rootDir));
     final AnalysisContextManager analysisContextManager =
         AnalysisContextManager(packagePath);
 
diff --git a/test/add/common/version_resolution_test.dart b/test/add/common/version_resolution_test.dart
index b526dab..5e73a86 100644
--- a/test/add/common/version_resolution_test.dart
+++ b/test/add/common/version_resolution_test.dart
@@ -28,6 +28,12 @@
     server.serve('foo', '3.1.0');
     server.serve('foo', '2.5.0');
 
+    await pubAdd(
+      args: ['foo', '--dry-run'],
+      output: allOf(
+        contains('> foo 3.5.0 (was 3.2.1)'),
+      ),
+    );
     await pubAdd(args: ['foo']);
 
     await d.appDir(dependencies: {'foo': '^3.5.0', 'bar': '1.0.0'}).validate();
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index f8d34c3..f350065 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -325,6 +325,12 @@
 Command: dart pub fail
 Platform: $OS
 
+---- $SANDBOX/empty/pubspec.yaml ----
+<No pubspec.yaml>
+---- End pubspec.yaml ----
+---- $SANDBOX/empty/pubspec.lock ----
+<No pubspec.lock>
+---- End pubspec.lock ----
 ---- Log transcript ----
 FINE: Pub 3.1.2+3
 ERR : Bad state: Pub has crashed