Merge remote-tracking branch 'origin/master' into cherry_pick_cache_warning
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3ce8efc..9940883 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,3 +10,5 @@
     directory: /
     schedule:
       interval: monthly
+    labels:
+      - autosubmit
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml
index ac3e456..dd7bbbc 100644
--- a/.github/workflows/no-response.yml
+++ b/.github/workflows/no-response.yml
@@ -1,12 +1,10 @@
 # A workflow to close issues where the author hasn't responded to a request for
-# more information; see https://github.com/godofredoc/no-response for docs.
+# more information; see https://github.com/actions/stale.
 
 name: No Response
 
-# Both `issue_comment` and `scheduled` event types are required.
+# Run as a daily cron.
 on:
-  issue_comment:
-    types: [created]
   schedule:
     # Every day at 8am
     - cron: '0 8 * * *'
@@ -14,21 +12,24 @@
 # All permissions not specified are set to 'none'.
 permissions:
   issues: write
+  pull-requests: write
 
 jobs:
-  noResponse:
+  no-response:
     runs-on: ubuntu-latest
     if: ${{ github.repository_owner == 'dart-lang' }}
     steps:
-      - uses: godofredoc/no-response@0ce2dc0e63e1c7d2b87752ceed091f6d32c9df09
+      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84
         with:
-          responseRequiredLabel: "needs-info"
-          responseRequiredColor: 4774bc
-          daysUntilClose: 14
-          # Comment to post when closing an Issue for lack of response.
-          closeComment: >
-            Without additional information we're not able to resolve this issue,
-            so it will be closed at this time. You're still free to add more
-            info and respond to any questions above, though. We'll reopen the
-            issue if you do. Thanks for your contribution!
-          token: ${{ github.token }}
+          days-before-stale: -1
+          days-before-close: 14
+          stale-issue-label: "needs-info"
+          close-issue-message: >
+            Without additional information we're not able to resolve this issue.
+            Feel free to add more info or respond to any questions above and we
+            can reopen the case. Thanks for your contribution!
+          stale-pr-label: "needs-info"
+          close-pr-message: >
+            Without additional information we're not able to resolve this PR.
+            Feel free to add more info or respond to any questions above.
+            Thanks for your contribution!
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 2717cf0..91b3aac 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -24,8 +24,8 @@
       matrix:
         sdk: [dev]
     steps:
-      - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
-      - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46
+      - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
+      - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f
         with:
           sdk: ${{ matrix.sdk }}
       - id: install
@@ -52,8 +52,8 @@
         sdk: [dev]
         shard: [0, 1, 2, 3, 4, 5, 6]
     steps:
-      - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
-      - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46
+      - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
+      - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f
         with:
           sdk: ${{ matrix.sdk }}
       - name: Install dependencies
diff --git a/doc/cache_layout.md b/doc/cache_layout.md
new file mode 100644
index 0000000..8cd76a7
--- /dev/null
+++ b/doc/cache_layout.md
@@ -0,0 +1,245 @@
+# The Pub cache
+
+The Pub cache is where pub stores downloaded packages and globally activated
+packages.
+
+The information in this document is informational, and can be used for
+understanding the cache, but we strongly encourage all manipulation of the cache
+happens though the `dart pub`/`flutter pub` commands to avoid relying on
+accidental properties of the cache that might be broken in the future.
+
+See [system_cache](../lib/src/system_cache.dart) for implementation of top-level
+cache conventions.
+
+## Location
+
+The global default pub-cache is located at:
+ * `$HOME/.pub_cache` on Linux and Mac OS,
+ * `%LOCALAPPDATA%/Pub/Cache` on Windows.
+
+Prior to Flutter 3.8.0, the Flutter SDK declared `PUB_CACHE=$FLUTTER_ROOT/.pub_cache` overriding the default global pub-cache.
+
+The default location of the pub-cache can be overridden using the environment variable `PUB_CACHE`.
+
+For the remainder of this document we refer to the location of the pub-cache as `$PUB_CACHE`.```
+
+## Layout
+
+The layout of the pub cache has evolved over time, and where possible we strive
+for backwards and forwards compatibility where possible, such that a new and an
+old sdk can share the same cache.
+
+Here are the top-level folders you can find in a Pub cache.
+
+```plaintext
+$PUB_CACHE/
+├── global_packages/  # Globally activated packages
+├── bin/              # Executables compiled from globally activated packages.
+├── git/              # Cloned git packages
+├── hosted/           # Hosted package downloads
+├── hosted-hashes/    # Hashes of hosted packages
+├── log/              # Logs after crashes and --verbose
+├── README.md         # Short description of the folder
+└── _temp/            # Package downloads are extracted here, and moved atomically.
+```
+
+Prior to Dart 2.15.0 pub would also store credentials in the pub-cache. They are now
+stored in a platform specific config dir:
+
+* On Linux `$XDG_CONFIG_HOME/dart/pub-credentials.json` if `$XDG_CONFIG_HOME` is
+  defined, otherwise `$HOME/.config/dart/pub-credentials.json`
+* On Mac OS: `$HOME/Library/Application Support/dart/pub-credentials.json`
+* On Windows: `%APPDATA%/dart/pub-credentials.json`
+
+### Hosted
+
+The `hosted/` folder contains one folder per repository that Pub has retrieved packages from.
+
+See [hosted](../lib/src/source/hosted.dart) for details.
+
+```plaintext
+$PUB_CACHE/hosted
+├── pub.dartlang.org
+├── pub.dev
+└── pub.flutter-io.cn
+```
+
+Before Dart 2.19 pub would by default download from `pub.dartlang.org`. This was
+changed to `pub.dev`. The two sites are mirrors and should always be identical.
+We decided to make the switch when we introduced content-hashes, because they
+anyway required redownloading of all packages to calculate the hashes.
+
+The url of the repository is encoded to a directory name with a weird URI-like
+encoding. This is a mistake that seems costly to fix, but is worth being aware
+of.
+
+Each repository folder has a sub-folder per `$package-$version/` that is
+downloaded from that repository:
+
+```plaintext
+$PUB_CACHE/hosted/pub.dev/
+├── .cache/
+├── args-2.3.2/
+├── retry-1.0.0/
+├── yaml-3.1.1/
+├── yaml_edit-2.0.2/
+└── yaml_edit-2.1.0/
+```
+
+A package name can always be used as a file-name (TODO: should we have a length-restriction on package-names? https://github.com/dart-lang/pub/issues/3895).
+
+A serialized version string can always be encoded as a file-name.
+
+These subfolders contain the content of the packages as they are extracted from
+the package archives. These are extracted in `.pub_cache/_temp` and moved here
+atomically, hence, packages here are always fully extracted.
+
+The `.cache/` folder is storing the last version listing response for each
+package:
+
+```plaintext
+$PUB_CACHE/hosted/pub.dev/.cache
+├── args-versions.json
+├── retry-versions.json
+├── yaml_edit-versions.json
+└── yaml-versions.json
+```
+
+These are used as a heuristic to speed up version resolution. They are
+timestamped with the time of retrieval.
+
+(This should arguably have been called something like `$PUB_CACHE/hosted-version-listings` to separate cleanly from the package downloads).
+
+Adding further files or folders inside `hosted/` unless the start with a '.' will break
+the `dart pub cache clean` command from older SDKs and should be avoided. (It assumes all folders/files are packages that need to be restored).
+
+The `$PUB_CACHE/hosted-hashes/` folder has a file per package-version with the sha256 hash of the downloaded archive:
+
+```plaintext
+$PUB_CACHE/hosted-hashes/
+└── pub.dev/
+    ├── args-2.3.2.sha256
+    ├── retry-1.0.0.sha256
+    ├── yaml-3.1.1.sha256
+    ├── yaml_edit-2.0.2.sha256
+    └── yaml_edit-2.1.0.sha256
+```
+
+These are used to ensure the integrity of the relation between a `pubspec.lock` file and
+the cache.
+
+* If a version-listing shows another hash, the package is redownloaded.
+* If a `pubspec.lock` file shows another hash the package is redownloaded.
+
+`$PUB_CACHE/hosted-hashes/` was introduced in Dart 2.19.0.
+
+## Git
+
+The `$PUB_CACHE/git/` folder has checkouts of the git repositories containing git dependencies.
+
+See [git](../lib/src/source/git.dart) for details.
+
+A git dependency is declared using:
+ * `url` (required)
+ * `ref` (optional, defaults to the default branch)
+ * `path` (optional, defaults to `.`)
+
+Note that we have the entire checkout, even though a package can be nested
+deeper inside using `path`. Two packages can share the same checkout.
+
+It is laid out as this example:
+
+```plaintext
+$PUB_CACHE/git/
+├── cache/
+│   ├── pana-72b499ded128c6590fbda1b7e87de1c8bbb38a04/
+│   └── pub-d666e8aee885cce49978e27a66c99ee08ce3995f/
+├── pana-bab826581f3f7a0604022f2043490a3b501e785e/
+├── pub-75c671c7d65db43f197b55419a8519906a611730/
+└── pub-c4e9ddc888c3aa89ef4462f0c4298929191e32b9/
+```
+
+The `$PUB_CACHE/git/cache/` folder contains a "bare" checkout of each git-url (just the ). The
+folders are `$PUB_CACHE/git/cache/$name-$hash/` where `$name` is derived from base-name of the
+git url (without `.git`). and `$hash` is the sha1 of the git-url. This makes
+them recognizable and unique.
+
+The other sub-folders are the actual checkouts. They are clones of respective the `$PUB_CACHE/git/cache/$name-$hash/`
+folders checked out at a specific `ref`. The name is `$PUB_CACHE/git/$name-$resolvedRef/` where
+`resolvedRef` is the commit-id that `ref` resolves to.
+
+## Global packages
+
+The `$PUB_CACHE/global_packages/` folder contains the globally activated
+packages.
+
+See [global_packages](../lib/src/global_packages.dart) for the implementation
+the global package conventions.
+
+The folder is laid out like in this example:
+
+```plaintext
+$PUB_CACHE/global_packages/
+├── stagehand/
+│   ├── bin/
+│   │   └── stagehand.dart-2.19.0.snapshot
+│   ├── .dart_tool/
+│   │   └── package_config.json
+│   ├── incremental
+│   └── pubspec.lock
+└── mono_repo/
+    ├── bin/
+    │   ├── mono_repo.dart-2.18.4.snapshot
+    │   ├── mono_repo.dart-3.0.0-0.0.dev.snapshot
+    │   └── mono_repo.dart-3.0.0-55.0.dev.snapshot
+    ├── .dart_tool/
+    │   └── package_config.json
+    ├── incremental
+    └── pubspec.lock
+```
+
+There can only be one globally activated package with a given name at the same
+time.
+
+Each globally installed package has its own folder with a `pubspec.lock` and a
+`.dart_tool/package_config.json`.
+
+The `pubspec.lock` holds the current resolution for the activated package.
+
+The `bin/` folder contains precompiled snapshots - these are compilations of
+`bin/*.dart` files from the activated packages, suffixed by
+`-$sdkVersion.snapshot`. Several snapshots can exist if the same globally
+activated package is used by several sdk-versions (TODO: This does have some
+limitations, and we should probably rethink this). A re-activation of the
+package will delete all the existing snapshots.
+
+The `incremental` is used while compiling them. (TODO: We should probably remove
+this after succesful compilation https://github.com/dart-lang/pub/issues/3896).
+
+For packages activated with `--source=path` the lockfile is special-cased to just point
+to the activated path, and `.dart_tool/package_config.json`, snapshots are
+stored in that folder.
+
+The `$PUB_CACHE/bin/` folder contains "binstubs" that are small executable
+scripts that will run the precompiled snapshots.
+
+By default one binstub is generated per `executable` in the `pubspec.yaml` of an
+activated package. The binstub contains decodable information about which
+package it belongs to, so it can be deleted when a package is `deactivated` and
+a helpful message can be shown in case of conflicts.
+
+If the snapshot doesn't exist, the binstub will attempt to create it by invoking
+`dart pub global run`.
+
+```plaintext
+$PUB_CACHE/bin/
+├── mono_repo
+└── stagehand
+```
+
+## Logs
+
+When pub crashes or is run with `--verbose` it will create a
+`$PUB_CACHE/log/pub_log.txt` with the dart sdk version, platform, `$PUB_CACHE`,
+`$PUB_HOSTED_URL`, `pubspec.yaml`, `pubspec.lock`, current command, verbose log and
+stack-trace.
diff --git a/doc/repository-spec-v2.md b/doc/repository-spec-v2.md
index 9104428..5c19cb1 100644
--- a/doc/repository-spec-v2.md
+++ b/doc/repository-spec-v2.md
@@ -11,11 +11,11 @@
 A custom package repository is identified by a _hosted-url_, like
 `https://pub.dev` or `https://some-server.com/prefix/pub/`.
 The _hosted-url_ always includes protocol `http://` or `https://`.
-For the purpose of this specification the _hosted-url_ should always be
+For the purpose of this specification, the _hosted-url_ should always be
 normalized such that it doesn't end with a slash (`/`). As all URL end-points
 described in this specification includes slash prefix.
 
-For the remainder of this specification the placeholder `<hosted-url>` will be
+For the remainder of this specification, the placeholder `<hosted-url>` will be
 used in place of a _hosted-url_ such as:
  * `https://pub.dev`
  * `https://some-server.com/prefix/pub`
@@ -64,7 +64,7 @@
 versions of the API to change responses.
 
 Clients are strongly encouraged to specify an `Accept` header. But for
-compatiblity will probably want to assume API version `2`,
+compatibility will probably want to assume API version `2`,
 if no `Accept` header is specified.
 
 
@@ -110,9 +110,9 @@
 }
 ```
 
-The `<message>` is intended to be a brief human readable explanation of what
+The `<message>` is intended to be a brief human-readable explanation of what
 when wrong and why the request failed. The `<code>` is a text string intended to
-allow clients to handle special cases without using regular expression to
+allow clients to handle special cases without using regular expressions to
 parse the `<message>`.
 
 
@@ -129,7 +129,7 @@
  * `dart pub token add <hosted-url>`
 
 This command will prompt the user for the `<token>` on stdin, reducing the risk
-that the `<token>` is accidentally stored in shell history. For security reasons
+that the `<token>` is accidentally stored in shell history. For security reasons,
 authentication can only be used when `<hosted-url>` uses HTTPS. For further
 details on token management see: `dart pub token --help`.
 
@@ -152,7 +152,7 @@
 a token for the given `<hosted-url>`, then the `dart pub` client knows for sure
 that the token it has stored for the given `<hosted-url>` is invalid.
 Hence, the `dart pub` client shall remove the token from local configuration.
-Hence, a server shall not send `401` in case where a token is valid, but does
+Hence, a server shall not send `401` in cases where a token is valid but does
 not have permissions to access the package in question.
 
 When receiving a `401` response the `dart pub` client shall:
@@ -180,7 +180,7 @@
 The `dart pub` will display the `message` in the terminal, so the user can
 discover that they need to navigate to `https://pub.example.com/manage-tokens`. 
 Once the user opens this URL in the browser, the server is then free to ask the
-user to sign-in using any browser-based authentication mechanism. Once signed-in
+user to sign-in using any browser-based authentication mechanism. Once signed in
 the server can allow the user to create a token and tell the user to copy/paste
 this into stdin for `dart pub token add pub.example.com`.
 
@@ -258,8 +258,8 @@
 The response (after following redirects) must be a gzipped TAR archive.
 
 The `archive_url` may be temporary and is allowed to include query-string
-parameters. This allows for the server to return signed-URLs for S3, GCS or
-other blob storage service. If temporary URLs are returned it is wise to not set
+parameters. This allows for the server to return signed URLs for S3, GCS, or
+other blob storage services. If temporary URLs are returned it is wise to not set
 expiration to less than 25 minutes (to allow for retries and clock drift).
 
 The `archive_sha256` should be the hex-encoded sha256 checksum of the file at
@@ -267,7 +267,7 @@
 integrity of the downloaded archive.
 
 The `archive_sha256` also provides an easy way for clients to detect if
-something has changed on the server. In the absense of this field the client can
+something has changed on the server. In the absence of this field, the client can
 still download the archive to obtain a checksum and detect changes to the
 archive.
 
@@ -276,7 +276,7 @@
 `archive_url` is requested. Example: if `https://pub.example.com/path` returns
 an `archive_url = 'https://pub.example.com/path/...'` then the request for
 `https://pub.example.com/path/...` will include `Authorization` header.
-This would however, not be case if the same server returned
+This would however, not be the case if the same server returned
 `archive_url = 'https://pub.example.com/blob/...'`.
 
 
@@ -303,7 +303,7 @@
 }
 ```
 
-To publish a package a HTTP `GET` request for
+To publish a package an HTTP `GET` request for
 `<hosted-url>/api/packages/versions/new` is made. This request returns an
 `<multipart-upload-url>` and a dictionary of fields. To upload the package
 archive a multi-part `POST` request is made to `<multipart-upload-url>` with
@@ -351,7 +351,7 @@
 The client shall then issue a `GET` request to `<finalize-upload-url>`. As with
 `archive_url` the client will only attach an `Authorization` if the
 `<hosted-url>` is a prefix of `<finalize-upload-url>`. If the server wants to
-accepts the uploaded package the server should respond:
+accept the uploaded package the server should respond:
 
 ```http
 HTTP/1.1 200 Ok
@@ -366,7 +366,7 @@
 The server is allowed to consider the publishing incomplete until the `GET`
 request for `<finalize-upload-url>` has been issued. Once this request has
 succeeded the package is considered successfully published. If the server has
-caches that need to expire before newly published packages becomes available,
+caches that need to expire before newly published packages become available,
 or it has other out-of-band approvals that need to be given it's reasonable to
 inform the user about this in the `<message>`.
 
@@ -382,14 +382,14 @@
 }
 ```
 
-This can be used to forbid git-dependencies in published packages, limit the
-archive size, or enforce any other repository specific constraints.
+This can be used to forbid git dependencies in published packages, limit the
+archive size, or enforce any other repository-specific constraints.
 
 This upload flow allows for archives to be uploaded directly to a signed POST
 URL for [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HTTPPOSTExamples.html),
 [GCS](https://cloud.google.com/storage/docs/xml-api/post-object-forms) or
 similar blob storage service. Both the
-`<multipart-upload-url>` and `<finalize-upload-url>` is allowed to contain
+`<multipart-upload-url>` and `<finalize-upload-url>` are allowed to contain
 query-string parameters, and both of these URLs need only be temporary.
 
 
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 7bb02a1..06a0260 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -2,6 +2,8 @@
 // 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 'dart:io';
+
 import 'package:args/args.dart';
 import 'package:collection/collection.dart';
 import 'package:path/path.dart' as p;
@@ -18,6 +20,7 @@
 import '../package.dart';
 import '../package_name.dart';
 import '../pubspec.dart';
+import '../sdk.dart';
 import '../solver.dart';
 import '../source/git.dart';
 import '../source/hosted.dart';
@@ -244,6 +247,15 @@
       writeTextFile(entrypoint.pubspecPath, newPubspecText);
     }
 
+    String? overridesFileContents;
+    final overridesPath =
+        p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename);
+    try {
+      overridesFileContents = readTextFile(overridesPath);
+    } on IOException {
+      overridesFileContents = null;
+    }
+
     /// 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.
@@ -253,6 +265,8 @@
             newPubspecText,
             cache.sources,
             location: Uri.parse(entrypoint.pubspecPath),
+            overridesFileContents: overridesFileContents,
+            overridesLocation: Uri.file(overridesPath),
           ),
         )
         .acquireDependencies(
@@ -612,6 +626,9 @@
             {
               'dependencies': {
                 packageName: parsedDescriptor,
+              },
+              'environment': {
+                'sdk': sdk.version.toString(),
               }
             },
             cache.sources,
diff --git a/lib/src/command/cache_add.dart b/lib/src/command/cache_add.dart
index 763856e..85c2c5d 100644
--- a/lib/src/command/cache_add.dart
+++ b/lib/src/command/cache_add.dart
@@ -72,15 +72,10 @@
     }
 
     Future<void> downloadVersion(id) async {
-      if (cache.contains(id)) {
-        // TODO(rnystrom): Include source and description if not hosted.
-        // See solve_report.dart for code to harvest.
+      final result = await cache.downloadPackage(id);
+      if (!result.didUpdate) {
         log.message('Already cached ${id.name} ${id.version}.');
-        return;
       }
-
-      // Download it.
-      await cache.downloadPackage(id);
     }
 
     if (argResults['all']) {
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index c56f413..ced8c00 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -53,7 +53,7 @@
   Future<void> runProtected() async {
     final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec);
 
-    final breakingPubspec = stripVersionUpperBounds(compatiblePubspec);
+    final breakingPubspec = stripVersionBounds(compatiblePubspec);
 
     final compatiblePackagesResult =
         await _tryResolve(compatiblePubspec, cache);
@@ -85,7 +85,7 @@
       if (package == null) return [];
       final lockFile = entrypoint.lockFile;
       final pubspec = upgradeType == _UpgradeType.multiBreaking
-          ? stripVersionUpperBounds(rootPubspec)
+          ? stripVersionBounds(rootPubspec)
           : Pubspec(
               rootPubspec.name,
               dependencies: rootPubspec.dependencies.values,
@@ -535,7 +535,7 @@
                     // This happens when we resolved a package from a legacy
                     // server not providing archive_sha256. As a side-effect of
                     // downloading the package we compute and store the sha256.
-                    package = await cache.downloadPackage(package);
+                    package = (await cache.downloadPackage(package)).packageId;
                   }
                 }
               } else {
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index 89db747..294df13 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -66,6 +66,8 @@
   /// Whether the publish requires confirmation.
   bool get force => argResults['force'];
 
+  bool get skipValidation => argResults['skip-validation'];
+
   LishCommand() {
     argParser.addFlag(
       'dry-run',
@@ -79,6 +81,12 @@
       negatable: false,
       help: 'Publish without confirmation if there are no errors.',
     );
+    argParser.addFlag(
+      'skip-validation',
+      negatable: false,
+      help:
+          'Publish without validation and resolution (this will ignore errors).',
+    );
     argParser.addOption(
       'server',
       help: 'The package server to which to upload this package.',
@@ -251,7 +259,13 @@
           'pubspec.');
     }
 
-    await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
+    if (!skipValidation) {
+      await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
+    } else {
+      log.warning(
+        'Running with `skip-validation`. No client-side validation is done.',
+      );
+    }
 
     var files = entrypoint.root.listFiles();
     log.fine('Archiving and publishing ${entrypoint.root.name}.');
@@ -267,10 +281,12 @@
         createTarGz(files, baseDir: entrypoint.rootDir).toBytes();
 
     // Validate the package.
-    var isValid = await _validate(
-      packageBytesFuture.then((bytes) => bytes.length),
-      files,
-    );
+    var isValid = skipValidation
+        ? true
+        : await _validate(
+            packageBytesFuture.then((bytes) => bytes.length),
+            files,
+          );
     if (!isValid) {
       overrideExitCode(exit_codes.DATA);
       return;
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 18ebf30..dee80a1 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -13,6 +13,7 @@
 import '../command_runner.dart';
 import '../entrypoint.dart';
 import '../io.dart';
+import '../lock_file.dart';
 import '../log.dart' as log;
 import '../package.dart';
 import '../package_name.dart';
@@ -142,13 +143,19 @@
     await log.spinner(
       'Resolving',
       () async {
-        final upgradablePackagesResult =
-            await _tryResolve(upgradablePubspec, cache);
+        final upgradablePackagesResult = await _tryResolve(
+          upgradablePubspec,
+          cache,
+          lockFile: entrypoint.lockFile,
+        );
         hasUpgradableResolution = upgradablePackagesResult != null;
         upgradablePackages = upgradablePackagesResult ?? [];
 
-        final resolvablePackagesResult =
-            await _tryResolve(resolvablePubspec, cache);
+        final resolvablePackagesResult = await _tryResolve(
+          resolvablePubspec,
+          cache,
+          lockFile: entrypoint.lockFile,
+        );
         hasResolvableResolution = resolvablePackagesResult != null;
         resolvablePackages = resolvablePackagesResult ?? [];
       },
@@ -386,11 +393,16 @@
 
 /// Try to solve [pubspec] return [PackageId]s in the resolution or `null` if no
 /// resolution was found.
-Future<List<PackageId>?> _tryResolve(Pubspec pubspec, SystemCache cache) async {
+Future<List<PackageId>?> _tryResolve(
+  Pubspec pubspec,
+  SystemCache cache, {
+  LockFile? lockFile,
+}) async {
   final solveResult = await tryResolveVersions(
     SolveType.upgrade,
     cache,
     Package.inMemory(pubspec),
+    lockFile: lockFile,
   );
 
   return solveResult?.packages;
@@ -546,31 +558,35 @@
     log.message(b.toString());
   }
 
-  var upgradable = rows
-      .where(
-        (row) =>
-            row.current != null &&
-            row.upgradable != null &&
-            row.current != row.upgradable &&
-            // Include transitive only, if we show them
-            (showTransitiveDependencies ||
-                hasKind(_DependencyKind.direct)(row) ||
-                hasKind(_DependencyKind.dev)(row)),
-      )
-      .length;
+  var upgradable = rows.where(
+    (row) {
+      final current = row.current;
+      final upgradable = row.upgradable;
+      return current != null &&
+          upgradable != null &&
+          current < upgradable &&
+          // Include transitive only, if we show them
+          (showTransitiveDependencies ||
+              hasKind(_DependencyKind.direct)(row) ||
+              hasKind(_DependencyKind.dev)(row));
+    },
+  ).length;
 
-  var notAtResolvable = rows
-      .where(
-        (row) =>
-            (row.current != null || !lockFileExists) &&
-            row.resolvable != null &&
-            row.upgradable != row.resolvable &&
-            // Include transitive only, if we show them
-            (showTransitiveDependencies ||
-                hasKind(_DependencyKind.direct)(row) ||
-                hasKind(_DependencyKind.dev)(row)),
-      )
-      .length;
+  var notAtResolvable = rows.where(
+    (row) {
+      final current = row.current;
+      final upgradable = row.upgradable;
+      final resolvable = row.resolvable;
+      return (current != null || !lockFileExists) &&
+          resolvable != null &&
+          upgradable != null &&
+          upgradable < resolvable &&
+          // Include transitive only, if we show them
+          (showTransitiveDependencies ||
+              hasKind(_DependencyKind.direct)(row) ||
+              hasKind(_DependencyKind.dev)(row));
+    },
+  ).length;
 
   if (!hasUpgradableResolution || !hasResolvableResolution) {
     log.message(mode.noResolutionText);
@@ -720,7 +736,7 @@
 
   @override
   Future<Pubspec> resolvablePubspec(Pubspec? pubspec) async {
-    return stripVersionUpperBounds(pubspec!);
+    return stripVersionBounds(pubspec!);
   }
 }
 
@@ -763,6 +779,11 @@
           _id.source == other._id.source &&
           _pubspec.version == other._pubspec.version;
 
+  bool operator <(_VersionDetails other) =>
+      _overridden == other._overridden &&
+      _id.source == other._id.source &&
+      _pubspec.version < other._pubspec.version;
+
   @override
   int get hashCode => Object.hash(_pubspec.version, _id.source, _overridden);
 }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 84fe11e..6c1dcda 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -3,7 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 
+import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml_edit/yaml_edit.dart';
 
@@ -176,7 +178,7 @@
   Future<void> _runUpgradeMajorVersions() async {
     final toUpgrade = _directDependenciesToUpgrade();
 
-    final resolvablePubspec = stripVersionUpperBounds(
+    final resolvablePubspec = stripVersionBounds(
       entrypoint.root.pubspec,
       stripOnly: toUpgrade,
     );
@@ -247,12 +249,23 @@
       }
     }
 
+    String? overridesFileContents;
+    final overridesPath =
+        p.join(entrypoint.rootDir, Pubspec.pubspecOverridesFilename);
+    try {
+      overridesFileContents = readTextFile(overridesPath);
+    } on IOException {
+      overridesFileContents = null;
+    }
+
     await entrypoint
         .withPubspec(
           Pubspec.parse(
             newPubspecText,
             cache.sources,
             location: Uri.parse(entrypoint.pubspecPath),
+            overridesFileContents: overridesFileContents,
+            overridesLocation: Uri.file(overridesPath),
           ),
         )
         .acquireDependencies(
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index db3df6f..ab8fed6 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -23,8 +23,8 @@
 import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
-import 'package_config.dart';
 import 'package_config.dart' show PackageConfig;
+import 'package_config.dart';
 import 'package_graph.dart';
 import 'package_name.dart';
 import 'pub_embeddable_command.dart';
@@ -32,6 +32,7 @@
 import 'sdk.dart';
 import 'solver.dart';
 import 'solver/report.dart';
+import 'solver/solve_suggestions.dart';
 import 'source/cached.dart';
 import 'source/unknown.dart';
 import 'system_cache.dart';
@@ -355,16 +356,30 @@
     }
 
     SolveResult result;
-    result = await log.progress('Resolving dependencies$suffix', () async {
-      _checkSdkConstraint(root.pubspec);
-      return resolveVersions(
-        type,
-        cache,
-        root,
-        lockFile: lockFile,
-        unlock: unlock ?? [],
+
+    try {
+      result = await log.progress('Resolving dependencies$suffix', () async {
+        _checkSdkConstraint(root.pubspec);
+        return resolveVersions(
+          type,
+          cache,
+          root,
+          lockFile: lockFile,
+          unlock: unlock ?? [],
+        );
+      });
+    } on SolveFailure catch (e) {
+      throw SolveFailure(
+        e.incompatibility,
+        suggestions: await suggestResolutionAlternatives(
+          this,
+          type,
+          e.incompatibility,
+          unlock ?? [],
+          cache,
+        ),
       );
-    });
+    }
 
     // We have to download files also with --dry-run to ensure we know the
     // archive hashes for downloaded files.
@@ -899,8 +914,8 @@
 
     // 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 || pkg.name == 'flutter_gen') continue;
+    for (final pkg in packageConfig.nonInjectedPackages) {
+      if (pkg.name == root.name) continue;
       final id = lockFile.packages[pkg.name];
       if (id == null) {
         assert(
diff --git a/lib/src/flutter_releases.dart b/lib/src/flutter_releases.dart
new file mode 100644
index 0000000..38e8ca8
--- /dev/null
+++ b/lib/src/flutter_releases.dart
@@ -0,0 +1,105 @@
+// Copyright (c) 2012, 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 'dart:convert';
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:http/http.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'http.dart';
+import 'log.dart';
+
+String get flutterReleasesUrl =>
+    Platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ??
+    'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json';
+
+// Retrieves all released versions of Flutter.
+Future<List<FlutterRelease>> _flutterReleases = () async {
+  final response = await retryForHttp(
+    'fetching available Flutter releases',
+    () => globalHttpClient.fetch(Request('GET', Uri.parse(flutterReleasesUrl))),
+  );
+  final decoded = jsonDecode(response.body);
+  if (decoded is! Map) throw FormatException('Bad response - should be a Map');
+  final releases = decoded['releases'];
+  if (releases is! List) {
+    throw FormatException('Bad response - releases should be a list.');
+  }
+  final result = <FlutterRelease>[];
+  for (final release in releases) {
+    final channel = {
+      'beta': Channel.beta,
+      'stable': Channel.stable,
+      'dev': Channel.dev
+    }[release['channel']];
+    if (channel == null) throw FormatException('Release with bad channel');
+    final dartVersion = release['dart_sdk_version'];
+    // Some releases don't have an associated dart version, ignore.
+    if (dartVersion is! String) continue;
+    final flutterVersion = release['version'];
+    if (flutterVersion is! String) throw FormatException('Not a string');
+    result.add(
+      FlutterRelease(
+        flutterVersion: Version.parse(flutterVersion),
+        dartVersion: Version.parse(dartVersion.split(' ').first),
+        channel: channel,
+      ),
+    );
+  }
+  return result
+      // Sort releases by channel and version.
+      .sorted((a, b) {
+        final compareChannels = b.channel.index - a.channel.index;
+        if (compareChannels != 0) return compareChannels;
+        return a.flutterVersion.compareTo(b.flutterVersion);
+      })
+      // Newest first.
+      .reversed
+      .toList();
+}();
+
+/// The "best" Flutter release for a given set of constraints is the first one
+/// in [_flutterReleases] that matches both the flutter and dart constraint.
+///
+/// Returns if no such release could be found.
+Future<FlutterRelease?> inferBestFlutterRelease(
+  Map<String, VersionConstraint> sdkConstraints,
+) async {
+  final List<FlutterRelease> flutterReleases;
+  try {
+    flutterReleases = await _flutterReleases;
+  } on Exception catch (e) {
+    fine('Failed retrieving the list of flutter-releases: $e');
+    return null;
+  }
+  return flutterReleases.firstWhereOrNull(
+    (release) =>
+        (sdkConstraints['flutter'] ?? VersionConstraint.any)
+            .allows(release.flutterVersion) &&
+        (sdkConstraints['dart'] ?? VersionConstraint.any)
+            .allows(release.dartVersion),
+  );
+}
+
+enum Channel {
+  stable,
+  beta,
+  dev,
+}
+
+/// A version of the Flutter SDK and its related Dart SDK.
+class FlutterRelease {
+  final Version flutterVersion;
+  final Version dartVersion;
+  final Channel channel;
+  FlutterRelease({
+    required this.flutterVersion,
+    required this.dartVersion,
+    required this.channel,
+  });
+  @override
+  toString() =>
+      'FlutterRelease(flutter=$flutterVersion, dart=$dartVersion, channel=$channel)';
+}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 93d6678..5ed3438 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -235,7 +235,7 @@
     } on SolveFailure catch (error) {
       for (var incompatibility
           in error.incompatibility.externalIncompatibilities) {
-        if (incompatibility.cause != IncompatibilityCause.noVersions) continue;
+        if (incompatibility.cause is! NoVersionsIncompatibilityCause) continue;
         if (incompatibility.terms.single.package.name != name) continue;
         // If the SolveFailure is caused by [dep] not
         // being available, report that as a [dataError].
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 8f510f6..086f93f 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -1190,6 +1190,6 @@
 ///
 /// Otherwise, wrap with single quotation, and use '\'' to insert single quote.
 String escapeShellArgument(String x) =>
-    RegExp(r'^[a-zA-Z0-9-_=@.]+$').stringMatch(x) == null
+    RegExp(r'^[a-zA-Z0-9-_=@.^]+$').stringMatch(x) == null
         ? "'${x.replaceAll(r'\', r'\\').replaceAll("'", r"'\''")}'"
         : x;
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index f725260..48de2fd 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -168,8 +168,9 @@
         packageEntries,
         (name, spec) {
           // Parse the version.
-          final versionEntry = _getStringEntry(spec, 'version');
-          final version = Version.parse(versionEntry);
+          final versionEntry =
+              _getEntry<YamlScalar>(spec, 'version', 'version string');
+          final version = _parseVersion(versionEntry);
 
           // Parse the source.
           final sourceName = _getStringEntry(spec, 'source');
@@ -243,7 +244,7 @@
       return fn();
     } on FormatException catch (e) {
       throw SourceSpanFormatException(
-        'Invalid $description: ${e.message}',
+        '$description: ${e.message}',
         span,
       );
     }
@@ -257,6 +258,14 @@
     );
   }
 
+  static Version _parseVersion(YamlNode node) {
+    return _parseNode(
+      node,
+      'version',
+      parse: Version.parse,
+    );
+  }
+
   static String _getStringEntry(YamlMap map, String key) {
     return _parseNode<String>(
       _getEntry<YamlScalar>(map, key, 'string'),
@@ -275,19 +284,19 @@
       final value = node.value;
       if (parse != null) {
         if (value is! String) {
-          _failAt('Expected a $typeDescription.', node);
+          _failAt('Expected a $typeDescription', node);
         }
         return _wrapFormatException(
-          'Expected a $typeDescription.',
+          'Expected a $typeDescription',
           node.span,
           () => parse(node.value),
         );
       } else if (value is T) {
         return value;
       }
-      _failAt('Expected a $typeDescription.', node);
+      _failAt('Expected a $typeDescription', node);
     }
-    _failAt('Expected a $typeDescription.', node);
+    _failAt('Expected a $typeDescription', node);
   }
 
   static void _parseEachEntry<K, V>(
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
index f5ef0b0..b215555 100644
--- a/lib/src/package_config.dart
+++ b/lib/src/package_config.dart
@@ -160,9 +160,13 @@
   //
   // See https://github.com/flutter/flutter/issues/73870 .
   Iterable<PackageConfigEntry> get nonInjectedPackages =>
-      packages.where((package) => package.name != 'flutter_gen');
+      packages.where((package) => !_isInjectedFlutterGenPackage(package));
 }
 
+bool _isInjectedFlutterGenPackage(PackageConfigEntry package) =>
+    package.name == 'flutter_gen' &&
+    package.rootUri.toString() == 'flutter_gen';
+
 class PackageConfigEntry {
   /// Package name.
   String name;
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index c46573a..3f57976 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -36,20 +36,22 @@
 }
 
 /// Returns new pubspec with the same dependencies as [original] but with the
-/// upper bounds of the constraints removed.
+/// the bounds of the constraints removed.
 ///
-/// If [stripOnly] is provided, only the packages whose names are in
-/// [stripOnly] will have their upper bounds removed. If [stripOnly] is
-/// not specified or empty, then all packages will have their upper bounds
-/// removed.
-Pubspec stripVersionUpperBounds(
+/// If [stripLower] is `false` (the default) only the upper bound is removed.
+///
+/// If [stripOnly] is provided, only the packages whose names are in [stripOnly]
+/// will have their bounds removed. If [stripOnly] is not specified or empty,
+/// then all packages will have their bounds removed.
+Pubspec stripVersionBounds(
   Pubspec original, {
   Iterable<String>? stripOnly,
+  bool stripLowerBound = false,
 }) {
   ArgumentError.checkNotNull(original, 'original');
   stripOnly ??= [];
 
-  List<PackageRange> stripUpperBounds(
+  List<PackageRange> stripBounds(
     Map<String, PackageRange> constrained,
   ) {
     final result = <PackageRange>[];
@@ -61,7 +63,9 @@
       if (stripOnly!.isEmpty || stripOnly.contains(packageRange.name)) {
         unconstrainedRange = PackageRange(
           packageRange.toRef(),
-          stripUpperBound(packageRange.constraint),
+          stripLowerBound
+              ? VersionConstraint.any
+              : stripUpperBound(packageRange.constraint),
         );
       }
       result.add(unconstrainedRange);
@@ -74,8 +78,8 @@
     original.name,
     version: original.version,
     sdkConstraints: original.sdkConstraints,
-    dependencies: stripUpperBounds(original.dependencies),
-    devDependencies: stripUpperBounds(original.devDependencies),
+    dependencies: stripBounds(original.dependencies),
+    devDependencies: stripBounds(original.devDependencies),
     dependencyOverrides: original.dependencyOverrides.values,
   );
 }
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index 74dbfa8..b8f68e7 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -4,6 +4,8 @@
 
 import 'dart:async';
 
+import 'package:pub_semver/pub_semver.dart';
+
 import 'lock_file.dart';
 import 'package.dart';
 import 'solver/failure.dart';
@@ -33,6 +35,7 @@
   Package root, {
   LockFile? lockFile,
   Iterable<String> unlock = const [],
+  Map<String, Version> sdkOverrides = const {},
 }) {
   lockFile ??= LockFile.empty();
   return VersionSolver(
@@ -41,6 +44,7 @@
     root,
     lockFile,
     unlock,
+    sdkOverrides: sdkOverrides,
   ).solve();
 }
 
diff --git a/lib/src/solver/failure.dart b/lib/src/solver/failure.dart
index ba4e69f..6c6b3f4 100644
--- a/lib/src/solver/failure.dart
+++ b/lib/src/solver/failure.dart
@@ -19,6 +19,8 @@
   /// it will have one term, which will be the root package.
   final Incompatibility incompatibility;
 
+  final String? suggestions;
+
   @override
   String get message => toString();
 
@@ -30,12 +32,12 @@
   PackageNotFoundException? get packageNotFound {
     for (var incompatibility in incompatibility.externalIncompatibilities) {
       var cause = incompatibility.cause;
-      if (cause is PackageNotFoundCause) return cause.exception;
+      if (cause is PackageNotFoundIncompatibilityCause) return cause.exception;
     }
     return null;
   }
 
-  SolveFailure(this.incompatibility)
+  SolveFailure(this.incompatibility, {this.suggestions})
       : assert(
           incompatibility.terms.isEmpty ||
               incompatibility.terms.single.package.isRoot,
@@ -44,7 +46,10 @@
   /// Describes how [incompatibility] was derived, and thus why version solving
   /// failed.
   @override
-  String toString() => _Writer(incompatibility).write();
+  String toString() => [
+        _Writer(incompatibility).write(),
+        if (suggestions != null) suggestions
+      ].join('\n');
 }
 
 /// A class that writes a human-readable description of the cause of a
diff --git a/lib/src/solver/incompatibility.dart b/lib/src/solver/incompatibility.dart
index 8b81501..15178b3 100644
--- a/lib/src/solver/incompatibility.dart
+++ b/lib/src/solver/incompatibility.dart
@@ -2,8 +2,6 @@
 // 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:pub_semver/pub_semver.dart';
-
 import '../package_name.dart';
 import 'incompatibility_cause.dart';
 import 'term.dart';
@@ -98,7 +96,7 @@
   /// for packages with the given names.
   @override
   String toString([Map<String, PackageDetail>? details]) {
-    if (cause == IncompatibilityCause.dependency) {
+    if (cause is DependencyIncompatibilityCause) {
       assert(terms.length == 2);
 
       var depender = terms.first;
@@ -108,20 +106,11 @@
 
       return '${_terse(depender, details, allowEvery: true)} depends on '
           '${_terse(dependee, details)}';
-    } else if (cause == IncompatibilityCause.useLatest) {
-      assert(terms.length == 1);
-
-      var forbidden = terms.last;
-      assert(forbidden.isPositive);
-
-      return 'the latest version of ${_terseRef(forbidden, details)} '
-          '(${VersionConstraint.any.difference(forbidden.constraint)}) '
-          'is required';
-    } else if (cause is SdkCause) {
+    } else if (cause is SdkIncompatibilityCause) {
       assert(terms.length == 1);
       assert(terms.first.isPositive);
 
-      var cause = this.cause as SdkCause;
+      var cause = this.cause as SdkIncompatibilityCause;
       var buffer = StringBuffer(_terse(terms.first, details, allowEvery: true));
       if (cause.noNullSafetyCause) {
         buffer.write(' doesn\'t support null safety');
@@ -135,25 +124,25 @@
         }
       }
       return buffer.toString();
-    } else if (cause == IncompatibilityCause.noVersions) {
+    } else if (cause is NoVersionsIncompatibilityCause) {
       assert(terms.length == 1);
       assert(terms.first.isPositive);
       return 'no versions of ${_terseRef(terms.first, details)} '
           'match ${terms.first.constraint}';
-    } else if (cause is PackageNotFoundCause) {
+    } else if (cause is PackageNotFoundIncompatibilityCause) {
       assert(terms.length == 1);
       assert(terms.first.isPositive);
 
-      var cause = this.cause as PackageNotFoundCause;
+      var cause = this.cause as PackageNotFoundIncompatibilityCause;
       return "${_terseRef(terms.first, details)} doesn't exist "
           '(${cause.exception.message})';
-    } else if (cause == IncompatibilityCause.unknownSource) {
+    } else if (cause is UnknownSourceIncompatibilityCause) {
       assert(terms.length == 1);
       assert(terms.first.isPositive);
       return '${terms.first.package.name} comes from unknown source '
           '"${terms.first.package.source}"';
-    } else if (cause == IncompatibilityCause.root) {
-      // [IncompatibilityCause.root] is only used when a package depends on the
+    } else if (cause is RootIncompatibilityCause) {
+      // [RootIncompatibilityCause] is only used when a package depends on the
       // entrypoint with an incompatible version, so we want to print the
       // entrypoint's actual version to make it clear why this failed.
       assert(terms.length == 1);
@@ -276,8 +265,8 @@
 
     var buffer =
         StringBuffer('${_terse(thisPositive, details, allowEvery: true)} ');
-    var isDependency = cause == IncompatibilityCause.dependency &&
-        other.cause == IncompatibilityCause.dependency;
+    var isDependency = cause is DependencyIncompatibilityCause &&
+        other.cause is DependencyIncompatibilityCause;
     buffer.write(isDependency ? 'depends on' : 'requires');
     buffer.write(' both $thisNegatives');
     if (thisLine != null) buffer.write(' ($thisLine)');
@@ -340,7 +329,7 @@
           priorPositives.map((term) => _terse(term, details)).join(' or ');
       buffer.write('if $priorString then ');
     } else {
-      var verb = prior.cause == IncompatibilityCause.dependency
+      var verb = prior.cause is DependencyIncompatibilityCause
           ? 'depends on'
           : 'requires';
       buffer.write('${_terse(priorPositives.first, details, allowEvery: true)} '
@@ -351,7 +340,7 @@
     if (priorLine != null) buffer.write(' ($priorLine)');
     buffer.write(' which ');
 
-    if (latter.cause == IncompatibilityCause.dependency) {
+    if (latter.cause is DependencyIncompatibilityCause) {
       buffer.write('depends on ');
     } else {
       buffer.write('requires ');
@@ -411,13 +400,13 @@
     } else {
       buffer.write(_terse(positives.first, details, allowEvery: true));
       buffer.write(
-        prior.cause == IncompatibilityCause.dependency
+        prior.cause is DependencyIncompatibilityCause
             ? ' depends on '
             : ' requires ',
       );
     }
 
-    if (latter.cause == IncompatibilityCause.unknownSource) {
+    if (latter.cause is UnknownSourceIncompatibilityCause) {
       var package = latter.terms.first.package;
       buffer.write('${package.name} ');
       if (priorLine != null) buffer.write('($priorLine) ');
@@ -429,12 +418,8 @@
     buffer.write('${_terse(latter.terms.first, details)} ');
     if (priorLine != null) buffer.write('($priorLine) ');
 
-    if (latter.cause == IncompatibilityCause.useLatest) {
-      var latest =
-          VersionConstraint.any.difference(latter.terms.single.constraint);
-      buffer.write('but the latest version ($latest) is required');
-    } else if (latter.cause is SdkCause) {
-      var cause = latter.cause as SdkCause;
+    if (latter.cause is SdkIncompatibilityCause) {
+      var cause = latter.cause as SdkIncompatibilityCause;
       if (cause.noNullSafetyCause) {
         buffer.write('which doesn\'t support null safety');
       } else {
@@ -446,11 +431,11 @@
           buffer.write('SDK version ${cause.constraint}');
         }
       }
-    } else if (latter.cause == IncompatibilityCause.noVersions) {
+    } else if (latter.cause is NoVersionsIncompatibilityCause) {
       buffer.write("which doesn't match any versions");
-    } else if (latter.cause is PackageNotFoundCause) {
+    } else if (latter.cause is PackageNotFoundIncompatibilityCause) {
       buffer.write("which doesn't exist "
-          '(${(latter.cause as PackageNotFoundCause).exception.message})');
+          '(${(latter.cause as PackageNotFoundIncompatibilityCause).exception.message})');
     } else {
       buffer.write('which is forbidden');
     }
diff --git a/lib/src/solver/incompatibility_cause.dart b/lib/src/solver/incompatibility_cause.dart
index c8f2db8..13047df 100644
--- a/lib/src/solver/incompatibility_cause.dart
+++ b/lib/src/solver/incompatibility_cause.dart
@@ -6,30 +6,14 @@
 
 import '../exceptions.dart';
 import '../language_version.dart';
+import '../package_name.dart';
 import '../sdk.dart';
+import '../source/sdk.dart';
 import 'incompatibility.dart';
 
 /// The reason an [Incompatibility]'s terms are incompatible.
-abstract class IncompatibilityCause {
-  const IncompatibilityCause._();
-
-  /// The incompatibility represents the requirement that the root package
-  /// exists.
-  static const IncompatibilityCause root = _Cause('root');
-
-  /// The incompatibility represents a package's dependency.
-  static const IncompatibilityCause dependency = _Cause('dependency');
-
-  /// The incompatibility represents the user's request that we use the latest
-  /// version of a given package.
-  static const IncompatibilityCause useLatest = _Cause('use latest');
-
-  /// The incompatibility indicates that the package has no versions that match
-  /// the given constraint.
-  static const IncompatibilityCause noVersions = _Cause('no versions');
-
-  /// The incompatibility indicates that the package has an unknown source.
-  static const IncompatibilityCause unknownSource = _Cause('unknown source');
+sealed class IncompatibilityCause {
+  const IncompatibilityCause();
 
   /// Human readable notice / information providing context for this
   /// incompatibility.
@@ -47,6 +31,50 @@
   String? get hint => null;
 }
 
+/// The incompatibility represents the requirement that the root package
+/// exists.
+class RootIncompatibilityCause extends IncompatibilityCause {
+  factory RootIncompatibilityCause() => const RootIncompatibilityCause._();
+  const RootIncompatibilityCause._();
+}
+
+/// The incompatibility represents a package's dependency.
+class DependencyIncompatibilityCause extends IncompatibilityCause {
+  final PackageRange depender;
+  final PackageRange target;
+  DependencyIncompatibilityCause(this.depender, this.target);
+
+  @override
+  String? get notice {
+    final dependerDescription = depender.description;
+    if (dependerDescription is SdkDescription) {
+      final targetConstraint = target.constraint;
+      if (targetConstraint is Version) {
+        return '''
+Note: ${target.name} is pinned to version $targetConstraint by ${depender.name} from the ${dependerDescription.sdk} SDK.
+See https://dart.dev/go/sdk-version-pinning for details.
+''';
+      }
+    }
+    return null;
+  }
+}
+
+/// The incompatibility indicates that the package has no versions that match
+/// the given constraint.
+class NoVersionsIncompatibilityCause extends IncompatibilityCause {
+  factory NoVersionsIncompatibilityCause() =>
+      const NoVersionsIncompatibilityCause._();
+  const NoVersionsIncompatibilityCause._();
+}
+
+/// The incompatibility indicates that the package has an unknown source.
+class UnknownSourceIncompatibilityCause extends IncompatibilityCause {
+  factory UnknownSourceIncompatibilityCause() =>
+      const UnknownSourceIncompatibilityCause._();
+  const UnknownSourceIncompatibilityCause._();
+}
+
 /// The incompatibility was derived from two existing incompatibilities during
 /// conflict resolution.
 class ConflictCause extends IncompatibilityCause {
@@ -58,22 +86,12 @@
   /// from which the target incompatibility was derived.
   final Incompatibility other;
 
-  ConflictCause(this.conflict, this.other) : super._();
-}
-
-/// A class for stateless [IncompatibilityCause]s.
-class _Cause extends IncompatibilityCause {
-  final String _name;
-
-  const _Cause(this._name) : super._();
-
-  @override
-  String toString() => _name;
+  ConflictCause(this.conflict, this.other);
 }
 
 /// The incompatibility represents a package's SDK constraint being
 /// incompatible with the current SDK.
-class SdkCause extends IncompatibilityCause {
+class SdkIncompatibilityCause extends IncompatibilityCause {
   /// The union of all the incompatible versions' constraints on the SDK.
   // TODO(zarah): Investigate if this can be non-nullable
   final VersionConstraint? constraint;
@@ -115,16 +133,16 @@
     return sdk.installMessage;
   }
 
-  SdkCause(this.constraint, this.sdk) : super._();
+  SdkIncompatibilityCause(this.constraint, this.sdk);
 }
 
 /// The incompatibility represents a package that couldn't be found by its
 /// source.
-class PackageNotFoundCause extends IncompatibilityCause {
+class PackageNotFoundIncompatibilityCause extends IncompatibilityCause {
   /// The exception indicating why the package couldn't be found.
   final PackageNotFoundException exception;
 
-  PackageNotFoundCause(this.exception) : super._();
+  PackageNotFoundIncompatibilityCause(this.exception);
 
   @override
   String? get hint => exception.hint;
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart
index 3dccd37..ce794b7 100644
--- a/lib/src/solver/package_lister.dart
+++ b/lib/src/solver/package_lister.dart
@@ -54,6 +54,8 @@
   /// reversed.
   final bool _isDowngrade;
 
+  final Map<String, Version> sdkOverrides;
+
   /// A map from dependency names to constraints indicating which versions of
   /// [_ref] have already had their dependencies on the given versions returned
   /// by [incompatibilitiesFor].
@@ -107,11 +109,15 @@
     this._overriddenPackages,
     this._allowedRetractedVersion, {
     bool downgrade = false,
+    this.sdkOverrides = const {},
   }) : _isDowngrade = downgrade;
 
   /// Creates a package lister for the root [package].
-  PackageLister.root(Package package, this._systemCache)
-      : _ref = PackageRef.root(package),
+  PackageLister.root(
+    Package package,
+    this._systemCache, {
+    required Map<String, Version>? sdkOverrides,
+  })  : _ref = PackageRef.root(package),
         // Treat the package as locked so we avoid the logic for finding the
         // boundaries of various constraints, which is useless for the root
         // package.
@@ -120,7 +126,8 @@
         _overriddenPackages =
             Set.unmodifiable(package.dependencyOverrides.keys),
         _isDowngrade = false,
-        _allowedRetractedVersion = null;
+        _allowedRetractedVersion = null,
+        sdkOverrides = sdkOverrides ?? {};
 
   /// Returns the number of versions of this package that match [constraint].
   Future<int> countVersions(VersionConstraint constraint) async {
@@ -202,7 +209,7 @@
       return [
         Incompatibility(
           [Term(id.toRange(), true)],
-          IncompatibilityCause.noVersions,
+          NoVersionsIncompatibilityCause(),
         )
       ];
     } on PackageNotFoundException {
@@ -212,7 +219,7 @@
       return [
         Incompatibility(
           [Term(id.toRange(), true)],
-          IncompatibilityCause.noVersions,
+          NoVersionsIncompatibilityCause(),
         )
       ];
     }
@@ -229,7 +236,7 @@
           return [
             Incompatibility(
               [Term(depender, true)],
-              SdkCause(
+              SdkIncompatibilityCause(
                 pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
                 sdk,
               ),
@@ -316,11 +323,12 @@
 
   /// Returns an [Incompatibility] that represents a dependency from [depender]
   /// onto [target].
-  Incompatibility _dependency(PackageRange depender, PackageRange target) =>
-      Incompatibility(
-        [Term(depender, true), Term(target, false)],
-        IncompatibilityCause.dependency,
-      );
+  Incompatibility _dependency(PackageRange depender, PackageRange target) {
+    return Incompatibility(
+      [Term(depender, true), Term(target, false)],
+      DependencyIncompatibilityCause(depender, target),
+    );
+  }
 
   /// If the version at [index] in [_versions] isn't compatible with the current
   /// version of [sdk], returns an [Incompatibility] indicating that.
@@ -356,7 +364,7 @@
 
     return Incompatibility(
       [Term(_ref.withConstraint(incompatibleVersions), true)],
-      SdkCause(sdkConstraint, sdk),
+      SdkIncompatibilityCause(sdkConstraint, sdk),
     );
   }
 
@@ -461,6 +469,7 @@
     if (constraint == null) return true;
 
     return sdk.isAvailable &&
-        constraint.effectiveConstraint.allows(sdk.version!);
+        constraint.effectiveConstraint
+            .allows(sdkOverrides[sdk.identifier] ?? sdk.version!);
   }
 }
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index 8c5a33f..f7a5d41 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -67,9 +67,10 @@
         if (id.source is CachedSource) {
           return await withDependencyType(_root.pubspec.dependencyType(id.name),
               () async {
-            return await cache.downloadPackage(
+            return (await cache.downloadPackage(
               id,
-            );
+            ))
+                .packageId;
           });
         }
         return id;
diff --git a/lib/src/solver/solve_suggestions.dart b/lib/src/solver/solve_suggestions.dart
new file mode 100644
index 0000000..ef95486
--- /dev/null
+++ b/lib/src/solver/solve_suggestions.dart
@@ -0,0 +1,278 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import '../command_runner.dart';
+import '../entrypoint.dart';
+import '../flutter_releases.dart';
+import '../io.dart';
+import '../package.dart';
+import '../package_name.dart';
+import '../pubspec.dart';
+import '../pubspec_utils.dart';
+import '../solver.dart';
+import '../source/hosted.dart';
+import '../system_cache.dart';
+import 'incompatibility.dart';
+import 'incompatibility_cause.dart';
+
+/// Looks through the root-[incompability] of a solve-failure and tries to see if
+/// the conflict could resolved by any of the following suggestions:
+/// * An update of the current SDK.
+/// * Any single change to a package constraint.
+/// * Removing the bounds on all constraints, changing less than 5 dependencies.
+/// * Running `pub upgrade --major versions`.
+///
+/// Returns a formatted list of suggestions, or the empty String if no
+/// suggestions were found.
+Future<String?> suggestResolutionAlternatives(
+  Entrypoint entrypoint,
+  SolveType type,
+  Incompatibility incompatibility,
+  Iterable<String> unlock,
+  SystemCache cache,
+) async {
+  final resolutionContext = _ResolutionContext(
+    entrypoint: entrypoint,
+    type: type,
+    cache: cache,
+    unlock: unlock,
+  );
+
+  final visited = <String>{};
+  final stopwatch = Stopwatch()..start();
+  final suggestions = <_ResolutionSuggestion>[];
+  void addSuggestionIfPresent(_ResolutionSuggestion? suggestion) {
+    if (suggestion != null) suggestions.add(suggestion);
+  }
+
+  for (final externalIncompatibility
+      in incompatibility.externalIncompatibilities) {
+    if (stopwatch.elapsed > Duration(seconds: 3)) {
+      // Never spend more than 3 seconds computing suggestions.
+      break;
+    }
+    final cause = externalIncompatibility.cause;
+    if (cause is SdkIncompatibilityCause) {
+      addSuggestionIfPresent(await resolutionContext.suggestSdkUpdate(cause));
+    } else {
+      for (final term in externalIncompatibility.terms) {
+        final name = term.package.name;
+
+        if (!visited.add(name)) {
+          continue;
+        }
+        addSuggestionIfPresent(
+          await resolutionContext.suggestSinglePackageUpdate(name),
+        );
+      }
+    }
+  }
+  if (suggestions.isEmpty) {
+    addSuggestionIfPresent(
+      await resolutionContext.suggestUnlockingAll(stripLowerBound: true) ??
+          await resolutionContext.suggestUnlockingAll(stripLowerBound: false),
+    );
+  }
+
+  if (suggestions.isEmpty) return null;
+  final tryOne = suggestions.length == 1
+      ? 'You can try the following suggestion to make the pubspec resolve:'
+      : 'You can try one of the following suggestions to make the pubspec resolve:';
+
+  suggestions.sort((a, b) => a.priority.compareTo(b.priority));
+
+  return '\n$tryOne\n${suggestions.take(5).map((e) => e.suggestion).join('\n')}';
+}
+
+class _ResolutionSuggestion {
+  final String suggestion;
+  final int priority;
+  _ResolutionSuggestion(this.suggestion, {this.priority = 0});
+}
+
+String packageAddDescription(Entrypoint entrypoint, PackageId id) {
+  final name = id.name;
+  final isDev = entrypoint.root.pubspec.devDependencies.containsKey(name);
+  final resolvedDescription = id.description;
+  final String descriptor;
+  final d = resolvedDescription.description.serializeForPubspec(
+    containingDir: Directory.current
+        .path // The add command will resolve file names relative to CWD.
+    // This currently should have no implications as we don't create suggestions
+    // for path-packages.
+    ,
+    languageVersion: entrypoint.root.pubspec.languageVersion,
+  );
+  if (d == null) {
+    descriptor = VersionConstraint.compatibleWith(id.version).toString();
+  } else {
+    descriptor = json.encode({
+      'version': VersionConstraint.compatibleWith(id.version).toString(),
+      id.source.name: d
+    });
+  }
+
+  final devPart = isDev ? 'dev:' : '';
+  return '$devPart$name:${escapeShellArgument(descriptor)}';
+}
+
+class _ResolutionContext {
+  final Entrypoint entrypoint;
+  final SolveType type;
+  final Iterable<String> unlock;
+  final SystemCache cache;
+  _ResolutionContext({
+    required this.entrypoint,
+    required this.type,
+    required this.cache,
+    required this.unlock,
+  });
+
+  /// If [cause] mentions an sdk, attempt resolving using another released
+  /// version of Flutter/Dart. Return that as a suggestion if found.
+  Future<_ResolutionSuggestion?> suggestSdkUpdate(
+    SdkIncompatibilityCause cause,
+  ) async {
+    final sdkName = cause.sdk.identifier;
+    if (!(sdkName == 'dart' || (sdkName == 'flutter' && runningFromFlutter))) {
+      // Only make sdk upgrade suggestions for Flutter and Dart.
+      return null;
+    }
+
+    final constraint = cause.constraint;
+    if (constraint == null) return null;
+
+    /// Find the most relevant Flutter release fullfilling the constraint.
+    final bestRelease =
+        await inferBestFlutterRelease({cause.sdk.identifier: constraint});
+    if (bestRelease == null) return null;
+    final result = await _tryResolve(
+      entrypoint.root.pubspec,
+      sdkOverrides: {
+        'dart': bestRelease.dartVersion,
+        'flutter': bestRelease.flutterVersion
+      },
+    );
+    if (result == null) {
+      return null;
+    }
+    return _ResolutionSuggestion(
+      runningFromFlutter
+          ? '* Try using the Flutter SDK version: ${bestRelease.flutterVersion}. '
+          :
+          // Here we assume that any Dart version included in a Flutter
+          // release can also be found as a released Dart SDK.
+          '* Try using the Dart SDK version: ${bestRelease.dartVersion}. See https://dart.dev/get-dart.',
+    );
+  }
+
+  /// Attempt another resolution with a relaxed constraint on [name]. If that
+  /// resolves, suggest upgrading to that version.
+  Future<_ResolutionSuggestion?> suggestSinglePackageUpdate(String name) async {
+    final originalRange = entrypoint.root.dependencies[name] ??
+        entrypoint.root.devDependencies[name];
+    if (originalRange == null ||
+        originalRange.description is! HostedDescription) {
+      // We can only relax constraints on hosted dependencies.
+      return null;
+    }
+    final originalConstraint = originalRange.constraint;
+    final relaxedPubspec = stripVersionBounds(
+      entrypoint.root.pubspec,
+      stripOnly: [name],
+      stripLowerBound: true,
+    );
+
+    final result = await _tryResolve(relaxedPubspec);
+    if (result == null) {
+      return null;
+    }
+    final resolvingPackage = result.packages.firstWhere((p) => p.name == name);
+
+    final addDescription = packageAddDescription(entrypoint, resolvingPackage);
+
+    var priority = 1;
+    var suggestion =
+        '* Try updating your constraint on $name: $topLevelProgram pub add $addDescription';
+    if (originalConstraint is VersionRange) {
+      final min = originalConstraint.min;
+      if (min != null) {
+        if (resolvingPackage.version < min) {
+          priority = 3;
+          suggestion =
+              '* Consider downgrading your constraint on $name: $topLevelProgram pub add $addDescription';
+        } else {
+          priority = 2;
+          suggestion =
+              '* Try upgrading your constraint on $name: $topLevelProgram pub add $addDescription';
+        }
+      }
+    }
+
+    return _ResolutionSuggestion(suggestion, priority: priority);
+  }
+
+  /// Attempt resolving with all version constraints relaxed. If that resolves,
+  /// return a corresponding suggestion to update.
+  Future<_ResolutionSuggestion?> suggestUnlockingAll({
+    required bool stripLowerBound,
+  }) async {
+    final originalPubspec = entrypoint.root.pubspec;
+    final relaxedPubspec =
+        stripVersionBounds(originalPubspec, stripLowerBound: stripLowerBound);
+
+    final result = await _tryResolve(relaxedPubspec);
+    if (result == null) {
+      return null;
+    }
+    final updatedPackageVersions = <PackageId>[];
+    for (final id in result.packages) {
+      final originalConstraint = (originalPubspec.dependencies[id.name] ??
+              originalPubspec.devDependencies[id.name])
+          ?.constraint;
+      if (originalConstraint != null) {
+        updatedPackageVersions.add(id);
+      }
+    }
+    if (stripLowerBound && updatedPackageVersions.length > 5) {
+      // Too complex, don't suggest.
+      return null;
+    }
+    if (stripLowerBound) {
+      updatedPackageVersions.sort((a, b) => a.name.compareTo(b.name));
+      final formattedConstraints = updatedPackageVersions
+          .map((e) => packageAddDescription(entrypoint, e))
+          .join(' ');
+      return _ResolutionSuggestion(
+        '* Try updating the following constraints: $topLevelProgram pub add $formattedConstraints',
+        priority: 4,
+      );
+    } else {
+      return _ResolutionSuggestion(
+        '* Try an upgrade of your constraints: $topLevelProgram pub upgrade --major-versions',
+        priority: 4,
+      );
+    }
+  }
+
+  /// Attempt resolving
+  Future<SolveResult?> _tryResolve(
+    Pubspec pubspec, {
+    Map<String, Version> sdkOverrides = const {},
+  }) async {
+    try {
+      return await resolveVersions(
+        type,
+        cache,
+        Package.inMemory(pubspec),
+        sdkOverrides: sdkOverrides,
+        lockFile: entrypoint.lockFile,
+        unlock: unlock,
+      );
+    } on SolveFailure {
+      return null;
+    }
+  }
+}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index e8a2346..afef413 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -75,6 +75,10 @@
   /// The set of packages for which the lockfile should be ignored.
   final Set<String> _unlock;
 
+  /// If present these represents the version of an SDK to assume during
+  /// resolution.
+  final Map<String, Version> _sdkOverrides;
+
   final _stopwatch = Stopwatch();
 
   VersionSolver(
@@ -82,8 +86,10 @@
     this._systemCache,
     this._root,
     this._lockFile,
-    Iterable<String> unlock,
-  )   : _dependencyOverrides = _root.dependencyOverrides,
+    Iterable<String> unlock, {
+    Map<String, Version> sdkOverrides = const {},
+  })  : _sdkOverrides = sdkOverrides,
+        _dependencyOverrides = _root.dependencyOverrides,
         _unlock = {...unlock};
 
   /// Finds a set of dependencies that match the root package's constraints, or
@@ -93,7 +99,7 @@
     _addIncompatibility(
       Incompatibility(
         [Term(PackageRange.root(_root), false)],
-        IncompatibilityCause.root,
+        RootIncompatibilityCause(),
       ),
     );
 
@@ -345,7 +351,7 @@
       _addIncompatibility(
         Incompatibility(
           [Term(candidate.toRef().withConstraint(VersionConstraint.any), true)],
-          IncompatibilityCause.unknownSource,
+          UnknownSourceIncompatibilityCause(),
         ),
       );
       return candidate.name;
@@ -367,7 +373,7 @@
       _addIncompatibility(
         Incompatibility(
           [Term(package.toRef().withConstraint(VersionConstraint.any), true)],
-          PackageNotFoundCause(error),
+          PackageNotFoundIncompatibilityCause(error),
         ),
       );
       return package.name;
@@ -387,7 +393,7 @@
         _addIncompatibility(
           Incompatibility(
             [Term(package, true)],
-            IncompatibilityCause.noVersions,
+            NoVersionsIncompatibilityCause(),
           ),
         );
         return package.name;
@@ -496,7 +502,13 @@
   PackageLister _packageLister(PackageRange package) {
     var ref = package.toRef();
     return _packageListers.putIfAbsent(ref, () {
-      if (ref.isRoot) return PackageLister.root(_root, _systemCache);
+      if (ref.isRoot) {
+        return PackageLister.root(
+          _root,
+          _systemCache,
+          sdkOverrides: _sdkOverrides,
+        );
+      }
 
       var locked = _getLocked(ref.name);
       if (locked != null && locked.toRef() != ref) locked = null;
@@ -516,6 +528,7 @@
         overridden,
         _getAllowedRetracted(ref.name),
         downgrade: _type == SolveType.downgrade,
+        sdkOverrides: _sdkOverrides,
       );
     });
   }
diff --git a/lib/src/source/cached.dart b/lib/src/source/cached.dart
index 9818146..933b08c 100644
--- a/lib/src/source/cached.dart
+++ b/lib/src/source/cached.dart
@@ -52,11 +52,6 @@
   /// the system cache.
   Future<Pubspec> describeUncached(PackageId id, SystemCache cache);
 
-  /// Determines if the package identified by [id] is already downloaded to the
-  /// system cache.
-  bool isInSystemCache(PackageId id, SystemCache cache) =>
-      dirExists(getDirectoryInCache(id, cache));
-
   /// Downloads the package identified by [id] to the system cache.
   Future<DownloadPackageResult> downloadToSystemCache(
     PackageId id,
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 6f6632c..1b28401 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -9,7 +9,7 @@
 import 'dart:typed_data';
 
 import 'package:collection/collection.dart'
-    show IterableExtension, IterableNullableExtension, ListEquality, maxBy;
+    show IterableExtension, IterableNullableExtension, maxBy;
 import 'package:crypto/crypto.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
@@ -554,23 +554,27 @@
       if (maxAge == null || now.difference(stat.modified) < maxAge) {
         try {
           final cachedDoc = jsonDecode(readTextFile(cachePath));
+          if (cachedDoc is! Map) {
+            throw FormatException('Broken cached version listing response');
+          }
           final timestamp = cachedDoc['_fetchedAt'];
-          if (timestamp is String) {
-            final parsedTimestamp = DateTime.parse(timestamp);
-            final cacheAge = DateTime.now().difference(parsedTimestamp);
-            if (maxAge != null && cacheAge > maxAge) {
-              // Too old according to internal timestamp - delete.
-              tryDeleteEntry(cachePath);
-            } else {
-              var res = _versionInfoFromPackageListing(
-                cachedDoc,
-                ref,
-                Uri.file(cachePath),
-                cache,
-              );
-              _responseCache[ref] = Pair(parsedTimestamp, res);
-              return res;
-            }
+          if (timestamp is! String) {
+            throw FormatException('Broken cached version listing response');
+          }
+          final parsedTimestamp = DateTime.parse(timestamp);
+          final cacheAge = DateTime.now().difference(parsedTimestamp);
+          if (maxAge != null && cacheAge > maxAge) {
+            // Too old according to internal timestamp - delete.
+            tryDeleteEntry(cachePath);
+          } else {
+            var res = _versionInfoFromPackageListing(
+              cachedDoc,
+              ref,
+              Uri.file(cachePath),
+              cache,
+            );
+            _responseCache[ref] = Pair(parsedTimestamp, res);
+            return res;
           }
         } on io.IOException {
           // Could not read the file. Delete if it exists.
@@ -857,28 +861,6 @@
     );
   }
 
-  /// Determines if the package identified by [id] is already downloaded to the
-  /// system cache and has the expected content-hash.
-  @override
-  bool isInSystemCache(PackageId id, SystemCache cache) {
-    if ((id.description as ResolvedHostedDescription).sha256 != null) {
-      try {
-        final cachedSha256 = readTextFile(hashPath(id, cache));
-        if (!const ListEquality().equals(
-          hexDecode(cachedSha256),
-          (id.description as ResolvedHostedDescription).sha256,
-        )) {
-          return false;
-        }
-      } on io.IOException {
-        // Most likely the hash file was not written, because we had a legacy
-        // entry.
-        return false;
-      }
-    }
-    return dirExists(getDirectoryInCache(id, cache));
-  }
-
   /// The system cache directory for the hosted source contains subdirectories
   /// for each separate repository URL that's used on the system.
   ///
@@ -1421,6 +1403,10 @@
     if (url == source.defaultUrl) {
       return null;
     }
+    if (languageVersion >=
+        LanguageVersion.firstVersionWithShorterHostedSyntax) {
+      return url;
+    }
     return {'url': url, 'name': packageName};
   }
 
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index 181e847..971adba 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -126,16 +126,6 @@
     }
   }
 
-  /// Determines if the system cache contains the package identified by [id].
-  bool contains(PackageId id) {
-    final source = id.source;
-
-    if (source is CachedSource) {
-      return source.isInSystemCache(id, this);
-    }
-    throw ArgumentError('Package $id is not cacheable.');
-  }
-
   /// Create a new temporary directory within the system cache.
   ///
   /// The system cache maintains its own temporary directory that it uses to
@@ -241,7 +231,7 @@
   ///
   /// Returns [id] with an updated [ResolvedDescription], this can be different
   /// if the content-hash changed while downloading.
-  Future<PackageId> downloadPackage(PackageId id) async {
+  Future<DownloadPackageResult> downloadPackage(PackageId id) async {
     final source = id.source;
     assert(source is CachedSource);
     final result = await (source as CachedSource).downloadToSystemCache(
@@ -258,7 +248,7 @@
     if (result.didUpdate) {
       maintainCache();
     }
-    return result.packageId;
+    return result;
   }
 
   /// Get the latest version of [package].
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart
index d22da8d..c29fa9e 100644
--- a/lib/src/validator/analyze.dart
+++ b/lib/src/validator/analyze.dart
@@ -22,12 +22,7 @@
         .where(dirExists);
     final result = await runProcess(
       Platform.resolvedExecutable,
-      [
-        'analyze',
-        '--fatal-infos',
-        ...dirsToAnalyze,
-        p.join(entrypoint.rootDir, 'pubspec.yaml')
-      ],
+      ['analyze', ...dirsToAnalyze, p.join(entrypoint.rootDir, 'pubspec.yaml')],
     );
     if (result.exitCode != 0) {
       final limitedOutput = limitLength(result.stdout.join('\n'), 1000);
diff --git a/pubspec.yaml b/pubspec.yaml
index d93fc33..7bc76e8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,7 +9,7 @@
   analyzer: ^5.1.0
   args: ^2.4.0
   async: ^2.6.1
-  cli_util: ^0.3.5
+  cli_util: ^0.4.0
   collection: ^1.15.0
   convert: ^3.0.2
   crypto: ^3.0.1
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
index f4a611b..8e958af 100644
--- a/test/add/common/add_test.dart
+++ b/test/add/common/add_test.dart
@@ -1081,4 +1081,26 @@
       )
     ]).validate();
   });
+
+  test('should take pubspec_overrides.yaml into account', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    await d.dir('bar', [d.libPubspec('bar', '1.0.0')]).create();
+    await d.appDir(
+      dependencies: {
+        'bar': '^1.0.0',
+      },
+    ).create();
+    await d.dir(appPath, [
+      d.pubspecOverrides({
+        'dependency_overrides': {
+          'bar': {'path': '../bar'}
+        }
+      })
+    ]).create();
+
+    await pubGet();
+
+    await pubAdd(args: ['foo'], output: contains('+ foo 1.0.0'));
+  });
 }
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart
index f6767a0..7be0095 100644
--- a/test/add/hosted/non_default_pub_server_test.dart
+++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -33,11 +33,43 @@
 
     await d.appDir(
       dependencies: {
+        'foo': {'version': '1.2.3', 'hosted': url}
+      },
+    ).validate();
+  });
+
+  test('Uses old syntax when needed', () async {
+    // Make the default server serve errors. Only the custom server should
+    // be accessed.
+    (await servePackages()).serveErrors();
+
+    final server = await startPackageServer();
+    server.serve('foo', '0.2.5');
+    server.serve('foo', '1.1.0');
+    server.serve('foo', '1.2.3');
+    final oldSyntaxSdkConstraint = {
+      'environment': {
+        'sdk': '>=2.14.0 <3.0.0' // Language version for old syntax.
+      },
+    };
+
+    await d.appDir(
+      dependencies: {},
+      pubspec: oldSyntaxSdkConstraint,
+    ).create();
+
+    final url = server.url;
+
+    await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]);
+
+    await d.appDir(
+      dependencies: {
         'foo': {
           'version': '1.2.3',
           'hosted': {'name': 'foo', 'url': url}
         }
       },
+      pubspec: oldSyntaxSdkConstraint,
     ).validate();
   });
 
@@ -75,18 +107,9 @@
 
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        },
-        'bar': {
-          'version': '3.2.3',
-          'hosted': {'name': 'bar', 'url': url}
-        },
-        'baz': {
-          'version': '1.3.5',
-          'hosted': {'name': 'baz', 'url': url}
-        }
+        'foo': {'version': '1.2.3', 'hosted': url},
+        'bar': {'version': '3.2.3', 'hosted': url},
+        'baz': {'version': '1.3.5', 'hosted': url}
       },
     ).validate();
   });
@@ -139,10 +162,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '^1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': '^1.2.3', 'hosted': url}
       },
     ).validate();
   });
@@ -170,10 +190,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '^1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': '^1.2.3', 'hosted': url}
       },
     ).validate();
   });
@@ -202,10 +219,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': 'any',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': 'any', 'hosted': url}
       },
     ).validate();
   });
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 480acae..cf504d6 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -99,6 +99,32 @@
     File(snapshot).parent.deleteSync(recursive: true);
   });
 
+  test('Can depend on package:flutter_gen', () async {
+    // Regression test for https://github.com/dart-lang/pub/issues/3314.
+    final server = await servePackages();
+    server.serve(
+      'flutter_gen',
+      '1.0.0',
+      contents: [
+        d.dir('bin', [d.file('flutter_gen.dart', 'main() {print("hi");}')])
+      ],
+    );
+
+    await d.appDir(
+      dependencies: {'flutter_gen': '^1.0.0'},
+    ).create();
+    await pubGet();
+    final buffer = StringBuffer();
+
+    await runEmbeddingToBuffer(
+      ['run', 'flutter_gen'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: getPubTestEnvironment(),
+    );
+    expect(buffer.toString(), contains('hi'));
+  });
+
   testWithGolden('run works, though hidden', (ctx) async {
     await servePackages();
     await d.dir(appPath, [
diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart
index ea4daba..fd30bb6 100644
--- a/test/embedding/ensure_pubspec_resolved.dart
+++ b/test/embedding/ensure_pubspec_resolved.dart
@@ -58,7 +58,7 @@
       final contents = json.decode(File(packageConfig).readAsStringSync());
       contents['packages'].add({
         'name': 'flutter_gen',
-        'rootUri': '.dart_tool/flutter_gen',
+        'rootUri': 'flutter_gen',
         'languageVersion': '2.8',
       });
       writeTextFile(packageConfig, json.encode(contents));
diff --git a/test/hosted/offline_test.dart b/test/hosted/offline_test.dart
index 5e54c76..da98d0c 100644
--- a/test/hosted/offline_test.dart
+++ b/test/hosted/offline_test.dart
@@ -118,10 +118,8 @@
       await pubCommand(
         command,
         args: ['--offline'],
-        error: equalsIgnoringWhitespace("""
-            Because myapp depends on foo >2.0.0 which doesn't match any
-              versions, version solving failed.
-          """),
+        error: contains('''
+Because myapp depends on foo >2.0.0 which doesn't match any versions, version solving failed.'''),
       );
     });
 
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart
index 3dbcf92..9fa7081 100644
--- a/test/lock_file_test.dart
+++ b/test/lock_file_test.dart
@@ -7,6 +7,7 @@
 import 'package:pub/src/source/hosted.dart';
 import 'package:pub/src/system_cache.dart';
 import 'package:pub_semver/pub_semver.dart';
+import 'package:source_span/source_span.dart';
 import 'package:test/test.dart' hide Description;
 import 'package:yaml/yaml.dart';
 
@@ -135,7 +136,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -149,7 +150,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -166,7 +167,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -184,7 +185,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -201,7 +202,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -218,7 +219,7 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -236,54 +237,54 @@
               sources,
             );
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
       test("throws if the old-style SDK constraint isn't a string", () {
         expect(
           () => LockFile.parse('sdk: 1.0', sources),
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
       test('throws if the old-style SDK constraint is invalid', () {
         expect(
           () => LockFile.parse('sdk: oops', sources),
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
       test("throws if the sdks field isn't a map", () {
         expect(
           () => LockFile.parse('sdks: oops', sources),
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
       test("throws if an sdk constraint isn't a string", () {
         expect(
           () => LockFile.parse('sdks: {dart: 1.0}', sources),
-          throwsFormatException,
+          throwsSourceSpanException,
         );
         expect(
           () {
             LockFile.parse('sdks: {dart: 1.0.0, flutter: 1.0}', sources);
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
       test('throws if an sdk constraint is invalid', () {
         expect(
           () => LockFile.parse('sdks: {dart: oops}', sources),
-          throwsFormatException,
+          throwsSourceSpanException,
         );
         expect(
           () {
             LockFile.parse('sdks: {dart: 1.0.0, flutter: oops}', sources);
           },
-          throwsFormatException,
+          throwsSourceSpanException,
         );
       });
 
@@ -411,3 +412,5 @@
     });
   });
 }
+
+final throwsSourceSpanException = throwsA(isA<SourceSpanException>());
diff --git a/test/outdated/outdated_test.dart b/test/outdated/outdated_test.dart
index 0ef5756..0757d0a 100644
--- a/test/outdated/outdated_test.dart
+++ b/test/outdated/outdated_test.dart
@@ -70,7 +70,9 @@
         },
       )
       ..serve('transitive', '1.2.3')
-      ..serve('dev_trans', '1.0.0');
+      ..serve('dev_trans', '1.0.0')
+      ..serve('retracted', '1.0.0')
+      ..serve('retracted', '1.0.1');
 
     await d.dir('local_package', [
       d.libDir('local_package'),
@@ -83,7 +85,8 @@
         'dependencies': {
           'foo': '^1.0.0',
           'bar': '^1.0.0',
-          'local_package': {'path': '../local_package'}
+          'local_package': {'path': '../local_package'},
+          'retracted': '^1.0.0',
         },
         'dev_dependencies': {'builder': '^1.0.0'},
       })
@@ -112,7 +115,10 @@
       ..serve('transitive', '2.0.0')
       ..serve('transitive2', '1.0.0')
       ..serve('transitive3', '1.0.0')
-      ..serve('dev_trans', '2.0.0');
+      ..serve('dev_trans', '2.0.0')
+      // Even though the current (and latest) version is retracted, it should be
+      // the one shown in the upgradable and resolvable columns.
+      ..retractPackageVersion('retracted', '1.0.1');
     await ctx.runOutdatedTests();
   });
 
diff --git a/test/pinned_dependency_hint_test.dart b/test/pinned_dependency_hint_test.dart
new file mode 100644
index 0000000..f1f4247
--- /dev/null
+++ b/test/pinned_dependency_hint_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2023, 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 'package:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+main() {
+  test('Gives hint when solve failure concerns a pinned flutter package',
+      () async {
+    await d.dir('flutter', [
+      d.dir('packages', [
+        d.dir(
+          'flutter_foo',
+          [
+            d.libPubspec('flutter_foo', '0.0.1', deps: {'tool': '1.0.0'})
+          ],
+        )
+      ]),
+      d.file('version', '1.2.3')
+    ]).create();
+    await servePackages()
+      ..serve('bar', '1.0.0', deps: {'tool': '^2.0.0'})
+      ..serve('tool', '1.0.0')
+      ..serve('tool', '2.0.0');
+
+    await d.appDir(
+      dependencies: {
+        'bar': 'any',
+        'flutter_foo': {'sdk': 'flutter'}
+      },
+    ).create();
+    await pubGet(
+      environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')},
+      error: contains(
+        'Note: tool is pinned to version 1.0.0 by flutter_foo from the flutter SDK.',
+      ),
+    );
+  });
+
+  test('Gives hint when solve failure concerns a pinned flutter package',
+      () async {
+    await d.dir('flutter', [
+      d.dir('packages', [
+        d.dir(
+          'flutter_foo',
+          [
+            d.libPubspec('flutter_foo', '0.0.1', deps: {'tool': '1.0.0'})
+          ],
+        )
+      ]),
+      d.file('version', '1.2.3')
+    ]).create();
+    await servePackages()
+      ..serve('tool', '1.0.0', deps: {'bar': '^2.0.0'})
+      ..serve('bar', '1.0.0');
+
+    await d.appDir(
+      dependencies: {
+        'bar': 'any',
+        'flutter_foo': {'sdk': 'flutter'}
+      },
+    ).create();
+    await pubGet(
+      environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')},
+      error: contains(
+        'Note: tool is pinned to version 1.0.0 by flutter_foo from the flutter SDK.',
+      ),
+    );
+  });
+}
diff --git a/test/sdk_test.dart b/test/sdk_test.dart
index b9a89d5..f409bc7 100644
--- a/test/sdk_test.dart
+++ b/test/sdk_test.dart
@@ -127,10 +127,8 @@
         await pubCommand(
           command,
           environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')},
-          error: equalsIgnoringWhitespace("""
-              Because myapp depends on foo ^1.0.0 from sdk which doesn't match
-                any versions, version solving failed.
-            """),
+          error: contains('''
+Because myapp depends on foo ^1.0.0 from sdk which doesn't match any versions, version solving failed.'''),
         );
       });
 
@@ -142,10 +140,8 @@
         ).create();
         await pubCommand(
           command,
-          error: equalsIgnoringWhitespace("""
-              Because myapp depends on foo from sdk which doesn't exist
-                (unknown SDK "unknown"), version solving failed.
-            """),
+          error: equalsIgnoringWhitespace('''
+Because myapp depends on foo from sdk which doesn't exist (unknown SDK "unknown"), version solving failed.'''),
           exitCode: exit_codes.UNAVAILABLE,
         );
       });
diff --git a/test/solve_suggestions_test.dart b/test/solve_suggestions_test.dart
new file mode 100644
index 0000000..a419243
--- /dev/null
+++ b/test/solve_suggestions_test.dart
@@ -0,0 +1,275 @@
+// Copyright (c) 2012, 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:shelf/shelf.dart';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('suggests an upgrade to the flutter sdk', () async {
+    await d.dir('flutter', [d.file('version', '1.2.3')]).create();
+    final server = await servePackages();
+    server.serve(
+      'foo',
+      '1.0.0',
+      pubspec: {
+        'environment': {'flutter': '>=3.3.0', 'sdk': '^2.17.0'}
+      },
+    );
+    server.handle(
+      '/flutterReleases',
+      (request) => Response.ok(releasesMockResponse),
+    );
+    await d.dir(appPath, [
+      d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0')
+    ]).create();
+    await pubGet(
+      error: contains('* Try using the Flutter SDK version: 3.3.2.'),
+      environment: {
+        '_PUB_TEST_SDK_VERSION': '2.17.0',
+        'FLUTTER_ROOT': path('flutter'),
+        '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases',
+        'PUB_ENVIRONMENT': 'flutter_cli',
+      },
+    );
+  });
+
+  test('suggests an upgrade to the dart sdk', () async {
+    final server = await servePackages();
+    server.serve(
+      'foo',
+      '1.0.0',
+      pubspec: {
+        'environment': {'sdk': '>=2.18.0 <2.18.1'}
+      },
+    );
+    server.handle(
+      '/flutterReleases',
+      (request) => Response.ok(releasesMockResponse),
+    );
+    await d.dir(appPath, [
+      d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0')
+    ]).create();
+    await pubGet(
+      error: contains('* Try using the Dart SDK version: 2.18.0'),
+      environment: {
+        '_PUB_TEST_SDK_VERSION': '2.17.0',
+        '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases',
+      },
+    );
+  });
+
+  test('suggests an upgrade or downgrade to a package constraint', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '^2.0.0'});
+    server.serve('foo', '0.9.0', deps: {'bar': '^1.0.0'});
+
+    server.serve('bar', '1.0.0');
+    server.serve('bar', '2.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '^1.0.0'},
+        devDeps: {'bar': '^1.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: allOf(
+        [
+          contains(
+            '* Consider downgrading your constraint on foo: dart pub add foo:^0.9.0',
+          ),
+          contains(
+            '* Try upgrading your constraint on bar: dart pub add dev:bar:^2.0.0',
+          ),
+        ],
+      ),
+    );
+  });
+
+  test('suggests an update to an empty package constraint', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '>1.0.0 <=0.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: allOf(
+        [
+          contains(
+            '* Try updating your constraint on foo: dart pub add foo:^1.0.0',
+          ),
+        ],
+      ),
+    );
+  });
+
+  test('suggests updates to multiple packages', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'});
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '1.0.0'},
+        devDeps: {'bar': '1.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try updating the following constraints: dart pub add dev:bar:^2.0.0 foo:^2.0.0',
+      ),
+    );
+  });
+
+  test('suggests a major upgrade if more than 5 needs to be upgraded',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo1', '1.0.0', deps: {'bar1': '2.0.0'});
+    server.serve('bar1', '1.0.0', deps: {'foo1': '2.0.0'});
+    server.serve('foo1', '2.0.0', deps: {'bar1': '2.0.0'});
+    server.serve('bar1', '2.0.0', deps: {'foo1': '2.0.0'});
+    server.serve('foo2', '1.0.0', deps: {'bar2': '2.0.0'});
+    server.serve('bar2', '1.0.0', deps: {'foo2': '2.0.0'});
+    server.serve('foo2', '2.0.0', deps: {'bar2': '2.0.0'});
+    server.serve('bar2', '2.0.0', deps: {'foo2': '2.0.0'});
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '1.0.0',
+          'bar': '1.0.0',
+          'foo1': '1.0.0',
+          'bar1': '1.0.0',
+          'foo2': '1.0.0',
+          'bar2': '1.0.0',
+        },
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try an upgrade of your constraints: dart pub upgrade --major-versions',
+      ),
+    );
+  });
+
+  test('suggests upgrades to non-default servers', () async {
+    final server = await servePackages();
+    final server2 = await startPackageServer();
+    server.serve(
+      'foo',
+      '1.0.0',
+      deps: {
+        'bar': {'version': '2.0.0', 'hosted': server2.url}
+      },
+    );
+
+    server2.serve('bar', '1.0.0');
+    server2.serve('bar', '2.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '^1.0.0',
+          'bar': {'version': '^1.0.0', 'hosted': server2.url},
+        },
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try upgrading your constraint on bar: dart pub add '
+        'bar:\'{"version":"^2.0.0","hosted":"${server2.url}"}\'',
+      ),
+    );
+    await pubAdd(
+      args: ['bar:{"version":"^2.0.0","hosted":"${server2.url}"}'],
+    );
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '^1.0.0',
+          'bar': {'version': '^2.0.0', 'hosted': server2.url},
+        },
+      )
+    ]).validate();
+  });
+}
+
+const releasesMockResponse = '''
+{
+  "base_url": "https://storage.googleapis.com/flutter_infra_release/releases",
+  "current_release": {
+    "beta": "096162697a9cdc79f4e47f7230d70935fa81fd24",
+    "dev": "13a2fb10b838971ce211230f8ffdd094c14af02c",
+    "stable": "e3c29ec00c9c825c891d75054c63fcc46454dca1"
+  },
+  "releases": [
+    {
+      "hash": "e3c29ec00c9c825c891d75054c63fcc46454dca1",
+      "channel": "stable",
+      "version": "3.3.2",
+      "dart_sdk_version": "2.18.1",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-09-14T15:06:55.724077Z",
+      "archive": "stable/linux/flutter_linux_3.3.2-stable.tar.xz",
+      "sha256": "a733a75ae07c42b2059a31fc9d64fabfae5dccd15770fa6b7f290e3f5f9c98e8"
+    },
+    {
+      "hash": "4f9d92fbbdf072a70a70d2179a9f87392b94104c",
+      "channel": "stable",
+      "version": "3.3.1",
+      "dart_sdk_version": "2.18.0",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-09-07T15:30:42.283999Z",
+      "archive": "stable/linux/flutter_linux_3.3.1-stable.tar.xz",
+      "sha256": "7cbcff0230affbe07a5ce82298044ac437e96aeba69f83656f9ed9a910a392e7"
+    },
+    {
+      "hash": "ffccd96b62ee8cec7740dab303538c5fc26ac543",
+      "channel": "stable",
+      "version": "3.3.0",
+      "dart_sdk_version": "2.18.0",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-08-30T17:22:12.916008Z",
+      "archive": "stable/linux/flutter_linux_3.3.0-stable.tar.xz",
+      "sha256": "a92a27aa6d4454d7a1cf9f8a0a56e0e5d6865f2cfcd21cf52e57f7922ad5d504"
+    },
+    {
+      "hash": "096162697a9cdc79f4e47f7230d70935fa81fd24",
+      "channel": "beta",
+      "version": "3.3.0-0.5.pre",
+      "dart_sdk_version": "2.18.0 (build 2.18.0-271.7.beta)",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-08-23T17:03:21.525151Z",
+      "archive": "beta/linux/flutter_linux_3.3.0-0.5.pre-beta.tar.xz",
+      "sha256": "8e07158a64a8ce79f9169cffe4ff23a486bdabb29401f13177672fae18de52d2"
+    }
+  ]
+}
+''';
diff --git a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
index 2729283..a0df7ef 100644
--- a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
+++ b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
@@ -98,11 +98,8 @@
 ## Section 10
 $ pub run -C myapp 'bin/app.dart'
 Building package executable...
-[STDERR] Failed to build test_pkg:app:
-[STDERR] myapp/bin/app.dart:1:1: Error: The specified language version is too high. The highest supported language version is 3.0.
-[STDERR] main() => print('Hi');
-[STDERR] ^
-[EXIT CODE] 1
+Built test_pkg:app.
+Hi
 
 -------------------------------- END OF OUTPUT ---------------------------------
 
diff --git a/test/testdata/goldens/help_test/pub publish --help.txt b/test/testdata/goldens/help_test/pub publish --help.txt
index 621c940..30ebb01 100644
--- a/test/testdata/goldens/help_test/pub publish --help.txt
+++ b/test/testdata/goldens/help_test/pub publish --help.txt
@@ -8,6 +8,7 @@
 -h, --help               Print this usage information.
 -n, --dry-run            Validate but do not publish the package.
 -f, --force              Publish without confirmation if there are no errors.
+    --skip-validation    Publish without validation and resolution (this will ignore errors).
 -C, --directory=<dir>    Run this in the directory <dir>.
 
 Run "pub help" to see global options.
diff --git a/test/testdata/goldens/outdated/outdated_test/newer versions available.txt b/test/testdata/goldens/outdated/outdated_test/newer versions available.txt
index 7d91029..4a31e13 100644
--- a/test/testdata/goldens/outdated/outdated_test/newer versions available.txt
+++ b/test/testdata/goldens/outdated/outdated_test/newer versions available.txt
@@ -54,6 +54,23 @@
       }
     },
     {
+      "package": "retracted",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "current": {
+        "version": "1.0.1"
+      },
+      "upgradable": {
+        "version": "1.0.1"
+      },
+      "resolvable": {
+        "version": "1.0.1"
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    },
+    {
       "package": "transitive",
       "kind": "transitive",
       "isDiscontinued": false,
@@ -110,6 +127,7 @@
 
 direct dependencies:
 foo           *1.2.3   *1.3.0      *2.0.0      3.0.0   
+retracted     *1.0.1   *1.0.1      *1.0.1      1.0.0   
 
 dev_dependencies:
 builder       *1.2.3   *1.3.0      2.0.0       2.0.0   
@@ -131,6 +149,7 @@
 
 direct dependencies:
 foo           *1.2.3   *1.3.0      *2.0.0      3.0.0   
+retracted     *1.0.1   *1.0.1      *1.0.1      1.0.0   
 
 dev_dependencies:
 builder       *1.2.3   *1.3.0      2.0.0       2.0.0   
@@ -154,6 +173,7 @@
 bar            1.0.0         1.0.0         1.0.0         1.0.0         
 foo            *1.2.3        *1.3.0        *2.0.0        3.0.0         
 local_package  0.0.1 (path)  0.0.1 (path)  0.0.1 (path)  0.0.1 (path)  
+retracted      *1.0.1        *1.0.1        *1.0.1        1.0.0         
 
 dev_dependencies:
 builder        *1.2.3        *1.3.0        2.0.0         2.0.0         
@@ -175,6 +195,7 @@
 
 direct dependencies:
 foo           *1.2.3   *1.3.0      *2.0.0      3.0.0        
+retracted     *1.0.1   *1.0.1      *1.0.1      1.0.0        
 
 dev_dependencies:
 builder       *1.2.3   *1.3.0      *2.0.0      3.0.0-alpha  
@@ -196,6 +217,7 @@
 
 direct dependencies:
 foo           *1.2.3   *1.3.0      3.0.0       3.0.0   
+retracted     *1.0.1   *1.0.1      *1.0.1      1.0.0   
 
 1 upgradable dependency is locked (in pubspec.lock) to an older version.
 To update it, use `dart pub upgrade`.
@@ -214,6 +236,7 @@
 
 direct dependencies:
 foo           *1.2.3   *1.3.0      *2.0.0      3.0.0   
+retracted     *1.0.1   *1.0.1      *1.0.1      1.0.0   
 
 dev_dependencies:
 builder       *1.2.3   *1.3.0      2.0.0       2.0.0   
@@ -248,6 +271,23 @@
       }
     },
     {
+      "package": "retracted",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "current": {
+        "version": "1.0.1"
+      },
+      "upgradable": {
+        "version": "1.0.1"
+      },
+      "resolvable": {
+        "version": "1.0.1"
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    },
+    {
       "package": "transitive",
       "kind": "transitive",
       "isDiscontinued": false,
diff --git a/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt b/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt
index 54bf561..3dd9640 100644
--- a/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt
+++ b/test/testdata/goldens/outdated/outdated_test/overridden dependencies.txt
@@ -161,9 +161,6 @@
 baz           *2.0.0 (overridden)  *1.0.0      2.0.0       2.0.0   
 foo           *1.0.1 (overridden)  *1.0.0      *1.0.0      2.0.0   
 
-3 upgradable dependencies are locked (in pubspec.lock) to older versions.
-To update these dependencies, use `dart pub upgrade`.
-
 1 dependency is constrained to a version that is older than a resolvable version.
 To update it, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
 
diff --git a/test/upgrade/upgrade_major_versions_test.dart b/test/upgrade/upgrade_major_versions_test.dart
index 40c23ea..691b221 100644
--- a/test/upgrade/upgrade_major_versions_test.dart
+++ b/test/upgrade/upgrade_major_versions_test.dart
@@ -277,5 +277,35 @@
         d.packageConfigEntry(name: 'bar', version: '4.0.0'),
       ]).validate();
     });
+
+    test('should take pubspec_overrides.yaml into account', () async {
+      await servePackages()
+        ..serve('foo', '1.0.0')
+        ..serve('foo', '2.0.0');
+      await d.dir('bar', [d.libPubspec('bar', '1.0.0')]).create();
+      await d.appDir(
+        dependencies: {
+          'foo': '^1.0.0',
+          'bar': '^1.0.0',
+        },
+      ).create();
+      await d.dir(appPath, [
+        d.pubspecOverrides({
+          'dependency_overrides': {
+            'bar': {'path': '../bar'}
+          }
+        })
+      ]).create();
+
+      await pubGet();
+
+      await pubUpgrade(
+        args: ['--major-versions'],
+        output: allOf([
+          contains('Changed 1 constraint in pubspec.yaml:'),
+          contains('foo: ^1.0.0 -> ^2.0.0'),
+        ]),
+      );
+    });
   });
 }
diff --git a/test/validator/analyze_test.dart b/test/validator/analyze_test.dart
index 5a1e8b4..55c9779 100644
--- a/test/validator/analyze_test.dart
+++ b/test/validator/analyze_test.dart
@@ -34,7 +34,7 @@
   });
 
   test(
-      'follows analysis_options.yaml and should warn if package contains errors in pubspec.yaml',
+      'follows analysis_options.yaml and should not warn if package contains only infos',
       () async {
     await d.dir(appPath, [
       d.libPubspec(
@@ -53,6 +53,32 @@
 ''')
     ]).create();
 
+    await expectValidation();
+  });
+
+  test(
+      'follows analysis_options.yaml and should warn if package contains warnings in pubspec.yaml',
+      () async {
+    await d.dir(appPath, [
+      d.libPubspec(
+        'test_pkg', '1.0.0',
+        sdk: '^3.0.0',
+        // Using http where https is recommended.
+        extras: {'repository': 'http://repo.org/'},
+      ),
+      d.file('LICENSE', 'Eh, do what you want.'),
+      d.file('README.md', "This package isn't real."),
+      d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'),
+      d.file('analysis_options.yaml', '''
+linter:
+  rules:
+    - secure_pubspec_urls
+analyzer:
+  errors:
+    secure_pubspec_urls: warning
+''')
+    ]).create();
+
     await expectValidation(
       error: allOf([
         contains(
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart
index 5a198ae..c1e5f75 100644
--- a/test/validator/language_version_test.dart
+++ b/test/validator/language_version_test.dart
@@ -2,6 +2,10 @@
 // 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 'dart:io';
+
+import 'package:pub/src/language_version.dart';
+import 'package:pub_semver/pub_semver.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -57,9 +61,12 @@
 
   group('should warn if it', () {
     test('opts in to a newer version.', () async {
+      final nextVersion =
+          Version.parse(Platform.version.split(' ').first).nextMajor;
       await setup(
         sdkConstraint: '^3.0.0',
-        libraryLanguageVersion: '3.1',
+        libraryLanguageVersion:
+            LanguageVersion.fromVersion(nextVersion).toString(),
       );
       await expectValidationWarning(
         'The language version override can\'t specify a version greater than the latest known language version',
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index 17c5be2..df3ae2b 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -200,8 +200,8 @@
 
   // Issue 1853
   test(
-      "produces a nice message for a locked dependency that's the only "
-      'version of its package', () async {
+      "produces a nice message for a locked dependency that's the only version of its package",
+      () async {
     await servePackages()
       ..serve('foo', '1.0.0', deps: {'bar': '>=2.0.0'})
       ..serve('bar', '1.0.0')
@@ -212,11 +212,9 @@
 
     await d.appDir(dependencies: {'foo': 'any', 'bar': '<2.0.0'}).create();
     await expectResolves(
-      error: equalsIgnoringWhitespace('''
-      Because myapp depends on foo any which depends on bar >=2.0.0,
-        bar >=2.0.0 is required.
-      So, because myapp depends on bar <2.0.0, version solving failed.
-    '''),
+      error: contains('''
+Because myapp depends on foo any which depends on bar >=2.0.0, bar >=2.0.0 is required.
+So, because myapp depends on bar <2.0.0, version solving failed.'''),
     );
   });
 }
@@ -336,11 +334,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because no versions of foo match ^2.0.0 and myapp depends on foo
-          >=1.0.0 <3.0.0, foo ^1.0.0 is required.
-        So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.
-      '''),
+        error: contains('''
+Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required.
+So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''),
       );
     });
 
@@ -357,11 +353,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because no versions of foo match ^2.0.0 and myapp depends on foo
-          >=1.0.0 <3.0.0, foo ^1.0.0 is required.
-        So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.
-      '''),
+        error: contains('''
+Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required.
+So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''),
       );
     });
 
@@ -378,10 +372,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version
-          solving failed.
-      '''),
+        error: contains(
+          'Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version solving failed.',
+        ),
       );
     });
 
@@ -441,10 +434,8 @@
 
     await d.appDir(dependencies: {'foo': '>=1.0.0 <2.0.0'}).create();
     await expectResolves(
-      error: equalsIgnoringWhitespace("""
-      Because myapp depends on foo ^1.0.0 which doesn't match any versions,
-        version solving failed.
-    """),
+      error: contains('''
+Because myapp depends on foo ^1.0.0 which doesn't match any versions, version solving failed.'''),
     );
   });
 
@@ -575,11 +566,10 @@
       ..serve('b', '1.0.0');
 
     await d.appDir(dependencies: {'a': 'any', 'b': '>1.0.0'}).create();
+
     await expectResolves(
-      error: equalsIgnoringWhitespace("""
-      Because myapp depends on b >1.0.0 which doesn't match any versions,
-        version solving failed.
-    """),
+      error: contains('''
+Because myapp depends on b >1.0.0 which doesn't match any versions, version solving failed.'''),
     );
   });
 
@@ -1115,11 +1105,10 @@
     ]).create();
 
     await expectResolves(
-      error: equalsIgnoringWhitespace('''
-      The current Dart SDK version is 3.1.2+3.
+      error: contains('''
+The current Dart SDK version is 3.1.2+3.
 
-      Because myapp requires SDK version 2.12.0, version solving failed.
-    '''),
+Because myapp requires SDK version 2.12.0, version solving failed.'''),
     );
   });