Repository: rust-lang/cargo (rust)

Issue:
Misleading error message when crates.io is `replace-with` a local source
### Problem

When using a local source via a `replace-with` in `config.toml` and there is a missing package, `cargo build` gives confusing errors saying that it searched `crates-io` for the package when it only looked in the local file system. The error it gives is identical to the error from an actually missing package:
```
error: no matching package named `<package name>` found
location searched: registry `crates-io`
required by package `example v0.1.0`
```

### Possible Solution(s)

If you run `cargo seach` it gives much more specific information:
```
$ cargo search ahash
error: crates-io is replaced with non-remote-registry source dir /.../cargo-bug/vendor;
include `--registry crates-io` to use crates.io
```
It would be great if the original error included this "crates-io is replaced with non-remote-registry source" remark.

### Steps


1. Unpack this zip archive: [cargo-bug.zip](https://github.com/user-attachments/files/17397502/cargo-bug.zip)
2. Run `cargo build` in root
3. Get error:
    ```
    error: no matching package named `ahasha` found
    location searched: registry `crates-io`
    required by package `example v0.1.0 (/home/rchatham/Documents/programming/tmp/cargo-bug)`
    ```

As an alternative, instead of unpacking, make an empty folder, cd into it, make the following files and then do the following shell instructions:

#### `Cargo.toml`
```toml
[package]
name = "example"
version = "0.1.0"
edition = "2021"

[dependencies]
ahash = { version = "^0.8.9" }
```

#### `.cargo/config.toml`

```toml
[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"
```

#### Shell instructions

Then:
```sh
mkdir src
touch src/lib.rs
mkdir vendor
cargo build
```


### Version

cargo 1.84.0-nightly (15fbd2f60 2024-10-08)
release: 1.84.0-nightly
commit-hash: 15fbd2f607d4defc87053b8b76bf5038f2483cf4
commit-date: 2024-10-08
host: x86_64-unknown-linux-gnu
libgit2: 1.8.1 (sys:0.19.0 vendored)
libcurl: 8.9.0-DEV (sys:0.4.74+curl-8.9.0 vendored ssl:OpenSSL/1.1.1w)
ssl: OpenSSL 1.1.1w  11 Sep 2023
os: Ubuntu 22.4.0 (jammy) [64-bit]

<!-- TRIAGEBOT_START -->

<!-- TRIAGEBOT_ASSIGN_START -->

<!-- TRIAGEBOT_ASSIGN_DATA_START$${"user":"chentodd"}$$TRIAGEBOT_ASSIGN_DATA_END -->

<!-- TRIAGEBOT_ASSIGN_END -->
<!-- TRIAGEBOT_END -->

Pull request:
fix: add source replacement info when no matching package found
### What does this PR try to resolve?

Fixes #14697 

I updated error message in `resolver/errors` with [Registry::describe_source](https://doc.rust-lang.org/nightly/nightly-rustc/cargo/core/registry/trait.Registry.html#tymethod.describe_source)

- Use message from `Registry::describe_source` as default message
- If the message is empty, use `SourceId` message

and I also updated related test cases.

### How should we test and review this PR?

1. Run following test cases:

- resolver-tests
- source_replacement
- build
- directory
- local_registry
- offline
- package
- publish
- registry
- update

2. Run [cargo-bug.zip](https://github.com/user-attachments/files/17493272/cargo-bug.zip)

- Unpack the zip archive
- Run `cargo build` in the root

following message should be seen:
```
error: no matching package named `ahasha` found
location searched: directory source `/home/foo/Desktop/cargo-bug/vendor` (which is replacing registry `crates-io`)
required by package `example v0.1.0 (/home/foo/Desktop/cargo-bug)`
```

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 862ce594f..af30753a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -569,18 +569,18 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.20"
+version = "4.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
 dependencies = [
  "clap_builder",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.5.20"
+version = "4.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
 dependencies = [
  "anstream",
  "anstyle",
@@ -591,9 +591,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.35"
+version = "4.5.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07a13ab5b8cb13dbe35e68b83f6c12f9293b2f601797b71bc9f23befdb329feb"
+checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39"
 dependencies = [
  "clap",
  "clap_lex",
@@ -3015,7 +3015,7 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
 
 [[package]]
 name = "rustfix"
-version = "0.8.8"
+version = "0.8.7"
 dependencies = [
  "anyhow",
  "proptest",
@@ -3434,12 +3434,12 @@ dependencies = [
 
 [[package]]
 name = "terminal_size"
-version = "0.4.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
+checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
 dependencies = [
  "rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ec9164af2..18b5015a0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,8 +36,8 @@ cargo-test-support = { version = "0.6.0", path = "crates/cargo-test-support" }
 cargo-util = { version = "0.2.14", path = "crates/cargo-util" }
 cargo-util-schemas = { version = "0.7.0", path = "crates/cargo-util-schemas" }
 cargo_metadata = "0.18.1"
-clap = "4.5.20"
-clap_complete = { version = "4.5.35", features = ["unstable-dynamic"] }
+clap = "4.5.18"
+clap_complete = { version = "4.5.32", features = ["unstable-dynamic"] }
 color-print = "0.3.6"
 core-foundation = { version = "0.10.0", features = ["mac_os_10_7_support"] }
 crates-io = { version = "0.40.4", path = "crates/crates-io" }
diff --git a/crates/rustfix/Cargo.toml b/crates/rustfix/Cargo.toml
index 21a70a59a..7b3d95a40 100644
--- a/crates/rustfix/Cargo.toml
+++ b/crates/rustfix/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustfix"
-version = "0.8.8"
+version = "0.8.7"
 authors = [
     "Pascal Hertleif <killercup@gmail.com>",
     "Oliver Schneider <oli-obk@users.noreply.github.com>",
diff --git a/crates/rustfix/src/lib.rs b/crates/rustfix/src/lib.rs
index 0283d1a93..a1e2c47cf 100644
--- a/crates/rustfix/src/lib.rs
+++ b/crates/rustfix/src/lib.rs
@@ -167,6 +167,8 @@ pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
         }
     }
 
+    let snippets = diagnostic.spans.iter().map(span_to_snippet).collect();
+
     let solutions: Vec<_> = diagnostic
         .children
         .iter()
@@ -202,7 +204,7 @@ pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
     } else {
         Some(Suggestion {
             message: diagnostic.message.clone(),
-            snippets: diagnostic.spans.iter().map(span_to_snippet).collect(),
+            snippets,
             solutions,
         })
     }
diff --git a/crates/rustfix/src/replace.rs b/crates/rustfix/src/replace.rs
index 4cdc7c01f..ed467dcba 100644
--- a/crates/rustfix/src/replace.rs
+++ b/crates/rustfix/src/replace.rs
@@ -24,7 +24,7 @@ impl State {
 }
 
 /// Span with a change [`State`].
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 struct Span {
     /// Start of this span in parent data
     start: usize,
@@ -34,17 +34,6 @@ struct Span {
     data: State,
 }
 
-impl std::fmt::Debug for Span {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let state = match self.data {
-            State::Initial => "initial",
-            State::Replaced(_) => "replaced",
-            State::Inserted(_) => "inserted",
-        };
-        write!(f, "({}, {}: {state})", self.start, self.end)
-    }
-}
-
 /// A container that allows easily replacing chunks of its data
 #[derive(Debug, Clone, Default)]
 pub struct Data {
@@ -108,82 +97,102 @@ impl Data {
         // [^empty]: Leading and trailing ones might be empty if we replace
         // the whole chunk. As an optimization and without loss of generality we
         // don't add empty parts.
-        let Some(index_of_part_to_split) = self
-            .parts
-            .iter()
-            .position(|p| !p.data.is_inserted() && p.start <= range.start && p.end >= range.end)
-        else {
-            tracing::debug!(
-                "no single slice covering {}..{}, current slices: {:?}",
-                range.start,
-                range.end,
-                self.parts,
-            );
-            return Err(Error::MaybeAlreadyReplaced(range));
-        };
+        let new_parts = {
+            let Some(index_of_part_to_split) = self.parts.iter().position(|p| {
+                !p.data.is_inserted() && p.start <= range.start && p.end >= range.end
+            }) else {
+                if tracing::enabled!(tracing::Level::DEBUG) {
+                    let slices = self
+                        .parts
+                        .iter()
+                        .map(|p| {
+                            (
+                                p.start,
+                                p.end,
+                                match p.data {
+                                    State::Initial => "initial",
+                                    State::Replaced(..) => "replaced",
+                                    State::Inserted(..) => "inserted",
+                                },
+                            )
+                        })
+                        .collect::<Vec<_>>();
+                    tracing::debug!(
+                        "no single slice covering {}..{}, current slices: {:?}",
+                        range.start,
+                        range.end,
+                        slices,
+                    );
+                }
 
-        let part_to_split = &self.parts[index_of_part_to_split];
+                return Err(Error::MaybeAlreadyReplaced(range));
+            };
 
-        // If this replacement matches exactly the part that we would
-        // otherwise split then we ignore this for now. This means that you
-        // can replace the exact same range with the exact same content
-        // multiple times and we'll process and allow it.
-        //
-        // This is currently done to alleviate issues like
-        // rust-lang/rust#51211 although this clause likely wants to be
-        // removed if that's fixed deeper in the compiler.
-        if part_to_split.start == range.start && part_to_split.end == range.end {
-            if let State::Replaced(ref replacement) = part_to_split.data {
-                if &**replacement == data {
-                    return Ok(());
+            let part_to_split = &self.parts[index_of_part_to_split];
+
+            // If this replacement matches exactly the part that we would
+            // otherwise split then we ignore this for now. This means that you
+            // can replace the exact same range with the exact same content
+            // multiple times and we'll process and allow it.
+            //
+            // This is currently done to alleviate issues like
+            // rust-lang/rust#51211 although this clause likely wants to be
+            // removed if that's fixed deeper in the compiler.
+            if part_to_split.start == range.start && part_to_split.end == range.end {
+                if let State::Replaced(ref replacement) = part_to_split.data {
+                    if &**replacement == data {
+                        return Ok(());
+                    }
                 }
             }
-        }
 
-        if part_to_split.data != State::Initial {
-            return Err(Error::AlreadyReplaced);
-        }
+            if part_to_split.data != State::Initial {
+                return Err(Error::AlreadyReplaced);
+            }
 
-        let mut new_parts = Vec::with_capacity(self.parts.len() + 2);
+            let mut new_parts = Vec::with_capacity(self.parts.len() + 2);
 
-        // Previous parts
-        if let Some(ps) = self.parts.get(..index_of_part_to_split) {
-            new_parts.extend_from_slice(ps);
-        }
+            // Previous parts
+            if let Some(ps) = self.parts.get(..index_of_part_to_split) {
+                new_parts.extend_from_slice(ps);
+            }
 
-        // Keep initial data on left side of part
-        if range.start > part_to_split.start {
-            new_parts.push(Span {
-                start: part_to_split.start,
-                end: range.start,
-                data: State::Initial,
-            });
-        }
+            // Keep initial data on left side of part
+            if range.start > part_to_split.start {
+                new_parts.push(Span {
+                    start: part_to_split.start,
+                    end: range.start,
+                    data: State::Initial,
+                });
+            }
 
-        // New part
-        new_parts.push(Span {
-            start: range.start,
-            end: range.end,
-            data: if insert_only {
-                State::Inserted(data.into())
-            } else {
-                State::Replaced(data.into())
-            },
-        });
-
-        // Keep initial data on right side of part
-        if range.end < part_to_split.end {
+            // New part
             new_parts.push(Span {
-                start: range.end,
-                end: part_to_split.end,
-                data: State::Initial,
+                start: range.start,
+                end: range.end,
+                data: if insert_only {
+                    State::Inserted(data.into())
+                } else {
+                    State::Replaced(data.into())
+                },
             });
-        }
 
-        // Following parts
-        if let Some(ps) = self.parts.get(index_of_part_to_split + 1..) {
-            new_parts.extend_from_slice(ps);
-        }
+            // Keep initial data on right side of part
+            if range.end < part_to_split.end {
+                new_parts.push(Span {
+                    start: range.end,
+                    end: part_to_split.end,
+                    data: State::Initial,
+                });
+            }
+
+            // Following parts
+            if let Some(ps) = self.parts.get(index_of_part_to_split + 1..) {
+                new_parts.extend_from_slice(ps);
+            }
+
+            new_parts
+        };
 
         self.parts = new_parts;
 
diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs
index 1bb1cb2c7..e6fff9093 100644
--- a/src/cargo/core/compiler/fingerprint/mod.rs
+++ b/src/cargo/core/compiler/fingerprint/mod.rs
@@ -2017,6 +2017,9 @@ where
                     let Ok(current_file_len) = fs::metadata(&path).map(|m| m.len()) else {
                         return Some(StaleItem::FailedToReadMetadata(path.to_path_buf()));
                     };
+                    let Ok(file) = File::open(path) else {
+                        return Some(StaleItem::MissingFile(path.to_path_buf()));
+                    };
                     if current_file_len != file_len {
                         return Some(StaleItem::FileSizeChanged {
                             path: path.to_path_buf(),
@@ -2024,9 +2027,6 @@ where
                             old_size: file_len,
                         });
                     }
-                    let Ok(file) = File::open(path) else {
-                        return Some(StaleItem::MissingFile(path.to_path_buf()));
-                    };
                     let Ok(checksum) = Checksum::compute(prior_checksum.algo, file) else {
                         return Some(StaleItem::UnableToReadFile(path.to_path_buf()));
                     };
diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs
index 7fbf4e22c..3c5504bdf 100644
--- a/src/cargo/core/resolver/errors.rs
+++ b/src/cargo/core/resolver/errors.rs
@@ -353,7 +353,13 @@ pub(super) fn activation_error(
             ));
             msg.push('\n');
         }
-        msg.push_str(&format!("location searched: {}\n", dep.source_id()));
+
+        let mut location_searched_msg = registry.describe_source(dep.source_id());
+        if location_searched_msg.is_empty() {
+            location_searched_msg = format!("{}", dep.source_id());
+        }
+
+        msg.push_str(&format!("location searched: {}\n", location_searched_msg));
         msg.push_str("required by ");
         msg.push_str(&describe_path_in_context(
             resolver_ctx,
diff --git a/src/cargo/ops/cargo_add/mod.rs b/src/cargo/ops/cargo_add/mod.rs
index 9e8e5d1b8..ac49f58e1 100644
--- a/src/cargo/ops/cargo_add/mod.rs
+++ b/src/cargo/ops/cargo_add/mod.rs
@@ -37,6 +37,7 @@ use crate::util::toml_mut::dependency::MaybeWorkspace;
 use crate::util::toml_mut::dependency::PathSource;
 use crate::util::toml_mut::dependency::Source;
 use crate::util::toml_mut::dependency::WorkspaceSource;
+use crate::util::toml_mut::is_sorted;
 use crate::util::toml_mut::manifest::DepTable;
 use crate::util::toml_mut::manifest::LocalManifest;
 use crate::CargoResult;
@@ -110,14 +111,10 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
         .map(TomlItem::as_table)
         .map_or(true, |table_option| {
             table_option.map_or(true, |table| {
-                table
-                    .get_values()
-                    .iter_mut()
-                    .map(|(key, _)| {
-                        // get_values key paths always have at least one key.
-                        key.remove(0)
-                    })
-                    .is_sorted()
+                is_sorted(table.get_values().iter_mut().map(|(key, _)| {
+                    // get_values key paths always have at least one key.
+                    key.remove(0)
+                }))
             })
         });
     for dep in deps {
diff --git a/src/cargo/ops/cargo_new.rs b/src/cargo/ops/cargo_new.rs
index 9de0f58dc..a3d08bb4d 100644
--- a/src/cargo/ops/cargo_new.rs
+++ b/src/cargo/ops/cargo_new.rs
@@ -1,6 +1,7 @@
 use crate::core::{Edition, Shell, Workspace};
 use crate::util::errors::CargoResult;
 use crate::util::important_paths::find_root_manifest_for_wd;
+use crate::util::toml_mut::is_sorted;
 use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
 use crate::util::{restricted_names, GlobalContext};
 use anyhow::{anyhow, Context as _};
@@ -994,7 +995,7 @@ fn update_manifest_with_new_member(
             }
         }
 
-        let was_sorted = members.iter().map(Value::as_str).is_sorted();
+        let was_sorted = is_sorted(members.iter().map(Value::as_str));
         members.push(display_path);
         if was_sorted {
             members.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str()));
diff --git a/src/cargo/util/toml_mut/dependency.rs b/src/cargo/util/toml_mut/dependency.rs
index 48ebe7ad5..5e0e437e8 100644
--- a/src/cargo/util/toml_mut/dependency.rs
+++ b/src/cargo/util/toml_mut/dependency.rs
@@ -14,6 +14,7 @@ use crate::core::SourceId;
 use crate::core::Summary;
 use crate::core::{Features, GitReference};
 use crate::util::toml::lookup_path_base;
+use crate::util::toml_mut::is_sorted;
 use crate::CargoResult;
 use crate::GlobalContext;
 
@@ -638,7 +639,7 @@ impl Dependency {
                             .collect::<Option<IndexSet<_>>>()
                     })
                     .unwrap_or_default();
-                let is_already_sorted = features.iter().is_sorted();
+                let is_already_sorted = is_sorted(features.iter());
                 features.extend(new_features.iter().map(|s| s.as_str()));
                 let features = if is_already_sorted {
                     features.into_iter().sorted().collect::<toml_edit::Value>()
diff --git a/src/cargo/util/toml_mut/mod.rs b/src/cargo/util/toml_mut/mod.rs
index 2cfa75568..1da707ea1 100644
--- a/src/cargo/util/toml_mut/mod.rs
+++ b/src/cargo/util/toml_mut/mod.rs
@@ -12,3 +12,19 @@
 pub mod dependency;
 pub mod manifest;
 pub mod upgrade;
+
+// Based on Iterator::is_sorted from nightly std; remove in favor of that when stabilized.
+pub fn is_sorted(mut it: impl Iterator<Item = impl PartialOrd>) -> bool {
+    let Some(mut last) = it.next() else {
+        return true;
+    };
+
+    for curr in it {
+        if curr < last {
+            return false;
+        }
+        last = curr;
+    }
+
+    true
+}
diff --git a/src/doc/src/guide/continuous-integration.md b/src/doc/src/guide/continuous-integration.md
index 0d69f9087..d9f14de9b 100644
--- a/src/doc/src/guide/continuous-integration.md
+++ b/src/doc/src/guide/continuous-integration.md
@@ -165,8 +165,6 @@ jobs:
     name: Latest Dependencies
     runs-on: ubuntu-latest
     continue-on-error: true
-    env:
-      CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: allow
     steps:
       - uses: actions/checkout@v4
       - run: rustup update stable && rustup default stable
@@ -174,9 +172,6 @@ jobs:
       - run: cargo build --verbose
       - run: cargo test --verbose
 ```
-Notes:
-- [`CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS`](../reference/config.md#resolverincompatible-rust-versions) is set to ensure the [resolver](../reference/resolver.md) doesn't limit selected dependencies because of your project's [Rust version](../reference/rust-version.md).
-
 For projects with higher risks of per-platform or per-Rust version failures,
 more combinations may want to be tested.
 
diff --git a/src/doc/src/reference/resolver.md b/src/doc/src/reference/resolver.md
index 3e40a7e7b..26c22250a 100644
--- a/src/doc/src/reference/resolver.md
+++ b/src/doc/src/reference/resolver.md
@@ -494,7 +494,7 @@ it so that it remains strictly acyclic.
 
 ## Resolver versions
 
-Different resolver behavior can be specified through the resolver
+A different feature resolver algorithm can be used by specifying the resolver
 version in `Cargo.toml` like this:
 
 ```toml
@@ -503,8 +503,12 @@ name = "my-package"
 version = "1.0.0"
 resolver = "2"
 ```
-- `"1"` (workspace, [`edition = "2015"`](manifest.md#the-edition-field) default)
-- `"2"` ([`edition = "2021"`](manifest.md#the-edition-field) default): Introduces changes in [feature
+
+The version `"1"` resolver is the original resolver that shipped with Cargo up to version 1.50.
+The default is `"2"` if the root package specifies [`edition = "2021"`](manifest.md#the-edition-field) or a newer edition.
+Otherwise the default is `"1"`.
+
+The version `"2"` resolver introduces changes in [feature
 unification](#features). See the [features chapter][features-2] for more
 details.
 


Test patch:
diff --git a/crates/resolver-tests/src/sat.rs b/crates/resolver-tests/src/sat.rs
index 93370a8c4..f04a585b2 100644
--- a/crates/resolver-tests/src/sat.rs
+++ b/crates/resolver-tests/src/sat.rs
@@ -1,4 +1,3 @@
-use std::collections::hash_map::Entry;
 use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
 use std::fmt::Write;
 
@@ -87,17 +86,16 @@ fn create_dependencies_vars<'a>(
             .or_default()
             .insert((kind, platform), solver.new_var());
 
-        let dep_feature_vars = var_for_is_dependencies_features_used
+        let dep_feature_var_map = dep
+            .features()
+            .iter()
+            .map(|&f| (f, solver.new_var()))
+            .collect();
+
+        var_for_is_dependencies_features_used
             .entry(name)
             .or_default()
-            .entry((kind, platform))
-            .or_default();
-
-        for &feature_name in dep.features() {
-            if let Entry::Vacant(entry) = dep_feature_vars.entry(feature_name) {
-                entry.insert(solver.new_var());
-            }
-        }
+            .insert((kind, platform), dep_feature_var_map);
     }
 
     for feature_values in pkg_features.values() {
@@ -111,14 +109,12 @@ fn create_dependencies_vars<'a>(
                 continue;
             };
 
-            for dep_feature_vars in var_for_is_dependencies_features_used
+            for dep_features_vars in var_for_is_dependencies_features_used
                 .get_mut(&dep_name)
                 .expect("feature dep name exists")
                 .values_mut()
             {
-                if let Entry::Vacant(entry) = dep_feature_vars.entry(dep_feature) {
-                    entry.insert(solver.new_var());
-                }
+                dep_features_vars.insert(dep_feature, solver.new_var());
             }
         }
     }
@@ -180,6 +176,7 @@ fn process_pkg_features(
     pkg_feature_var_map: &HashMap<InternedString, varisat::Var>,
     pkg_dependencies: &[Dependency],
     pkg_features: &FeatureMap,
+    check_dev_dependencies: bool,
 ) {
     let optional_dependencies = pkg_dependencies
         .iter()
@@ -202,7 +199,7 @@ fn process_pkg_features(
                 FeatureValue::Dep { dep_name } => {
                     // Add a clause for each dependency with the provided name (normal/build/dev with target)
                     for (&(dep_kind, _), &dep_var) in &var_for_is_dependencies_used[&dep_name] {
-                        if dep_kind == DepKind::Development {
+                        if dep_kind == DepKind::Development && !check_dev_dependencies {
                             continue;
                         }
                         solver.add_clause(&[pkg_feature_var.negative(), dep_var.positive()]);
@@ -226,7 +223,7 @@ fn process_pkg_features(
                     // Add clauses for each dependency with the provided name (normal/build/dev with target)
                     let dep_var_map = &var_for_is_dependencies_used[&dep_name];
                     for (&(dep_kind, dep_platform), &dep_var) in dep_var_map {
-                        if dep_kind == DepKind::Development {
+                        if dep_kind == DepKind::Development && !check_dev_dependencies {
                             continue;
                         }
 
@@ -273,22 +270,24 @@ fn process_compatible_dep_summaries(
         }
 
         let (name, kind, platform) = (dep.name_in_toml(), dep.kind(), dep.platform());
-        let dep_var = var_for_is_dependencies_used[&name][&(kind, platform)];
+        let dep_var_map = &var_for_is_dependencies_used[&name];
+        let dep_var = dep_var_map[&(kind, platform)];
+
         let dep_feature_var_map = &var_for_is_dependencies_features_used[&name][&(kind, platform)];
 
-        let compatible_pkg_ids = by_name
+        let compatible_summaries = by_name
             .get(&dep.package_name())
             .into_iter()
             .flatten()
             .filter(|s| dep.matches(s))
             .filter(|s| dep.features().iter().all(|f| s.features().contains_key(f)))
-            .map(|s| s.package_id())
+            .cloned()
             .collect::<Vec<_>>();
 
         // At least one compatible package should be activated
-        let dep_clause = compatible_pkg_ids
+        let dep_clause = compatible_summaries
             .iter()
-            .map(|id| var_for_is_packages_used[&id].positive())
+            .map(|s| var_for_is_packages_used[&s.package_id()].positive())
             .chain([dep_var.negative()])
             .collect::<Vec<_>>();
 
@@ -296,9 +295,11 @@ fn process_compatible_dep_summaries(
 
         for (&feature_name, &dep_feature_var) in dep_feature_var_map {
             // At least one compatible package with the additional feature should be activated
-            let dep_feature_clause = compatible_pkg_ids
+            let dep_feature_clause = compatible_summaries
                 .iter()
-                .filter_map(|id| var_for_is_packages_features_used[&id].get(&feature_name))
+                .filter_map(|s| {
+                    var_for_is_packages_features_used[&s.package_id()].get(&feature_name)
+                })
                 .map(|var| var.positive())
                 .chain([dep_feature_var.negative()])
                 .collect::<Vec<_>>();
@@ -310,9 +311,10 @@ fn process_compatible_dep_summaries(
             // For the selected package for this dependency, the `"default"` feature should be activated if it exists
             let mut dep_default_clause = vec![dep_var.negative()];
 
-            for &id in &compatible_pkg_ids {
-                let s_var = var_for_is_packages_used[&id];
-                let s_feature_var_map = &var_for_is_packages_features_used[&id];
+            for s in &compatible_summaries {
+                let s_pkg_id = s.package_id();
+                let s_var = var_for_is_packages_used[&s_pkg_id];
+                let s_feature_var_map = &var_for_is_packages_features_used[&s_pkg_id];
 
                 if let Some(s_default_feature_var) = s_feature_var_map.get(&INTERNED_DEFAULT) {
                     dep_default_clause
@@ -346,6 +348,8 @@ pub struct SatResolver {
 
 impl SatResolver {
     pub fn new<'a>(registry: impl IntoIterator<Item = &'a Summary>) -> Self {
+        let check_dev_dependencies = false;
+
         let mut by_name: HashMap<InternedString, Vec<Summary>> = HashMap::new();
         for pkg in registry {
             by_name.entry(pkg.name()).or_default().push(pkg.clone());
@@ -419,6 +423,7 @@ impl SatResolver {
                 &var_for_is_packages_features_used[&pkg_id],
                 pkg_dependencies,
                 pkg_features,
+                check_dev_dependencies,
             );
 
             process_compatible_dep_summaries(
@@ -429,7 +434,7 @@ impl SatResolver {
                 &var_for_is_packages_features_used,
                 &by_name,
                 pkg_dependencies,
-                false,
+                check_dev_dependencies,
             );
         }
 
diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs
index b7c876154..8031cbd9c 100644
--- a/tests/testsuite/build.rs
+++ b/tests/testsuite/build.rs
@@ -6043,7 +6043,7 @@ fn avoid_dev_deps() {
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `baz` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `bar v0.1.0 ([ROOT]/foo)`
 
 "#]])
diff --git a/tests/testsuite/directory.rs b/tests/testsuite/directory.rs
index 1db19eeb6..a9a8324b8 100644
--- a/tests/testsuite/directory.rs
+++ b/tests/testsuite/directory.rs
@@ -199,7 +199,7 @@ Caused by:
   no matching package found
   searched package name: `baz`
   perhaps you meant:      bar or foo
-  location searched: registry `crates-io`
+  location searched: directory source `[ROOT]/index` (which is replacing registry `crates-io`)
   required by package `bar v0.1.0`
 
 "#]])
@@ -283,7 +283,7 @@ fn not_there() {
         .with_status(101)
         .with_stderr_data(str![[r#"
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: directory source `[ROOT]/index` (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 
 "#]])
diff --git a/tests/testsuite/local_registry.rs b/tests/testsuite/local_registry.rs
index 0405ee876..a5ddcb48d 100644
--- a/tests/testsuite/local_registry.rs
+++ b/tests/testsuite/local_registry.rs
@@ -101,7 +101,7 @@ fn not_found() {
         .with_status(101)
         .with_stderr_data(str![[r#"
 [ERROR] no matching package named `baz` found
-location searched: registry `crates-io`
+location searched: `[ROOT]/registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]])
diff --git a/tests/testsuite/offline.rs b/tests/testsuite/offline.rs
index 19116f9fc..4da3e95b7 100644
--- a/tests/testsuite/offline.rs
+++ b/tests/testsuite/offline.rs
@@ -184,7 +184,7 @@ fn cargo_compile_offline_not_try_update() {
         .with_status(101)
         .with_stderr_data(str![[r#"
 [ERROR] no matching package named `not_cached_dep` found
-location searched: registry `crates-io`
+location searched: crates.io index
 required by package `bar v0.1.0 ([ROOT]/bar)`
 As a reminder, you're using offline mode (--offline) which can sometimes cause surprising resolution failures, if this error is too confusing you may wish to retry without the offline flag.
 
@@ -195,7 +195,7 @@ As a reminder, you're using offline mode (--offline) which can sometimes cause s
     p.change_file(".cargo/config.toml", "net.offline = true");
     p.cargo("check").with_status(101).with_stderr_data(str![[r#"
 [ERROR] no matching package named `not_cached_dep` found
-location searched: registry `crates-io`
+location searched: crates.io index
 required by package `bar v0.1.0 ([ROOT]/bar)`
 As a reminder, you're using offline mode (--offline) which can sometimes cause surprising resolution failures, if this error is too confusing you may wish to retry without the offline flag.
 
@@ -383,18 +383,16 @@ fn update_offline_not_cached() {
         )
         .file("src/main.rs", "fn main() {}")
         .build();
+
     p.cargo("update --offline")
         .with_status(101)
-        .with_stderr_data(
-            "\
+        .with_stderr_data(str![["
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: [..]
 required by package `foo v0.0.1 ([ROOT]/foo)`
-As a reminder, you're using offline mode (--offline) which can sometimes cause \
-surprising resolution failures, if this error is too confusing you may wish to \
-retry without the offline flag.
-",
-        )
+As a reminder, you're using offline mode (--offline) which can sometimes cause surprising resolution failures, if this error is too confusing you may wish to retry without the offline flag.
+
+"]])
         .run();
 }
 
@@ -756,7 +754,7 @@ fn main(){
         .with_stderr_data(
             str![[r#"
 [ERROR] no matching package named `present_dep` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 As a reminder, you're using offline mode (--offline) which can sometimes cause surprising resolution failures, if this error is too confusing you may wish to retry without the offline flag.
 
diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs
index 3984d5dae..e54d3a864 100644
--- a/tests/testsuite/package.rs
+++ b/tests/testsuite/package.rs
@@ -5312,7 +5312,7 @@ fn workspace_with_local_deps() {
 
 Caused by:
   no matching package named `level2` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `level1 v0.0.1 ([ROOT]/foo/level1)`
 
 "#]])
@@ -5496,7 +5496,7 @@ fn workspace_with_local_deps_packaging_one_fails() {
 
 Caused by:
   no matching package named `level2` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `level1 v0.0.1 ([ROOT]/foo/target/package/level1-0.0.1)`
 
 "#]])
@@ -5523,7 +5523,7 @@ fn workspace_with_local_deps_packaging_one_fails_nightly() {
 
 Caused by:
   no matching package named `level2` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `level1 v0.0.1 ([ROOT]/foo/target/package/level1-0.0.1)`
 
 "#]])
@@ -5589,7 +5589,7 @@ fn workspace_with_local_deps_packaging_one_bin_fails() {
 
 Caused by:
   no matching package named `level2` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `level1 v0.0.1 ([ROOT]/foo/level1)`
 
 "#]])
@@ -5809,7 +5809,7 @@ fn workspace_with_local_deps_index_mismatch() {
 
 Caused by:
   no matching package named `level2` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `level1 v0.0.1 ([ROOT]/foo/level1)`
 
 "#]])
@@ -6620,7 +6620,7 @@ fn unpublishable_dependency() {
 
 Caused by:
   no matching package named `dep` found
-  location searched: registry `alternative`
+  location searched: `alternative` index
   required by package `main v0.0.1 ([ROOT]/foo/main)`
 
 "#]])
diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs
index 88e78fae1..18fd5ebde 100644
--- a/tests/testsuite/publish.rs
+++ b/tests/testsuite/publish.rs
@@ -3932,7 +3932,7 @@ fn workspace_missing_dependency() {
 
 Caused by:
   no matching package named `a` found
-  location searched: registry `crates-io`
+  location searched: crates.io index
   required by package `b v0.0.1 ([ROOT]/foo/target/package/b-0.0.1)`
 
 "#]])
diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs
index e43e3c9dc..193e093bb 100644
--- a/tests/testsuite/registry.rs
+++ b/tests/testsuite/registry.rs
@@ -167,7 +167,7 @@ fn nonexistent_http() {
     nonexistent(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `nonexistent` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -178,7 +178,7 @@ fn nonexistent_git() {
     nonexistent(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `nonexistent` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -218,7 +218,7 @@ fn wrong_case_http() {
 [ERROR] no matching package found
 searched package name: `Init`
 perhaps you meant:      init
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -231,7 +231,7 @@ fn wrong_case_git() {
 [ERROR] no matching package found
 searched package name: `Init`
 perhaps you meant:      init
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -272,7 +272,7 @@ fn mis_hyphenated_http() {
 [ERROR] no matching package found
 searched package name: `mis_hyphenated`
 perhaps you meant:      mis-hyphenated
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -285,7 +285,7 @@ fn mis_hyphenated_git() {
 [ERROR] no matching package found
 searched package name: `mis_hyphenated`
 perhaps you meant:      mis-hyphenated
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]]);
@@ -468,7 +468,7 @@ fn update_registry_http() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `notyet` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -491,7 +491,7 @@ fn update_registry_git() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `notyet` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -551,7 +551,7 @@ fn package_with_path_deps_http() {
 
 Caused by:
   no matching package named `notyet` found
-  location searched: registry `crates-io`
+  location searched: `dummy-registry` index (which is replacing registry `crates-io`)
   required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -580,7 +580,7 @@ fn package_with_path_deps_git() {
 
 Caused by:
   no matching package named `notyet` found
-  location searched: registry `crates-io`
+  location searched: `dummy-registry` index (which is replacing registry `crates-io`)
   required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -925,7 +925,7 @@ fn yanks_in_lockfiles_are_ok_http() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -942,7 +942,7 @@ fn yanks_in_lockfiles_are_ok_git() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -994,7 +994,7 @@ fn yanks_in_lockfiles_are_ok_for_other_update_http() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -1017,7 +1017,7 @@ fn yanks_in_lockfiles_are_ok_for_other_update_git() {
         str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]],
@@ -3226,7 +3226,7 @@ fn unknown_index_version_error() {
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 
 "#]])
@@ -3769,7 +3769,7 @@ foo v0.1.0 ([ROOT]/foo)
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 
 "#]])
@@ -3863,7 +3863,7 @@ fn not_found_permutations() {
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `a-b_c` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.0.1 ([ROOT]/foo)`
 
 "#]])
diff --git a/tests/testsuite/source_replacement.rs b/tests/testsuite/source_replacement.rs
index 2e24bec2f..3bf390270 100644
--- a/tests/testsuite/source_replacement.rs
+++ b/tests/testsuite/source_replacement.rs
@@ -294,3 +294,46 @@ fn source_replacement_with_registry_url() {
 "#]])
         .run();
 }
+
+#[cargo_test]
+fn source_replacement_with_no_package_in_directoy() {
+    let p = project()
+        .file(
+            "Cargo.toml",
+            r#"
+                [package]
+                name = "foo"
+                version = "0.1.0"
+                edition = "2021"
+
+                [dependencies]
+                bar = { version = "^0.8.9" }
+            "#,
+        )
+        .file("src/lib.rs", "")
+        .build();
+
+    let root = paths::root();
+    t!(fs::create_dir(&root.join("vendor")));
+
+    let crates_io = setup_replacement(&format!(
+        r#"
+            [source.crates-io]
+            replace-with = "vendored-sources"
+
+            [source.vendored-sources]
+            directory = "vendor"
+        "#
+    ));
+
+    p.cargo("build")
+        .replace_crates_io(crates_io.index_url())
+        .with_status(101)
+        .with_stderr_data(str![[r#"
+[ERROR] no matching package named `bar` found
+location searched: directory source `[ROOT]/vendor` (which is replacing registry `crates-io`)
+required by package `foo v0.1.0 ([ROOT]/foo)`
+
+"#]])
+        .run();
+}
diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs
index 905259206..87c36293d 100644
--- a/tests/testsuite/update.rs
+++ b/tests/testsuite/update.rs
@@ -456,7 +456,7 @@ perhaps a crate was updated and forgotten to be re-vendored?
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `serde` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `bar v0.0.1 ([ROOT]/foo)`
 
 "#]])
@@ -997,7 +997,7 @@ Caused by:
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 
 "#]])
@@ -1017,7 +1017,7 @@ required by package `foo v0.1.0 ([ROOT]/foo)`
         .with_stderr_data(str![[r#"
 [UPDATING] `dummy-registry` index
 [ERROR] no matching package named `bar` found
-location searched: registry `crates-io`
+location searched: `dummy-registry` index (which is replacing registry `crates-io`)
 required by package `foo v0.1.0 ([ROOT]/foo)`
 
 "#]])