Repository: rust-lang/cargo (rust)

Issue:
`-Zpackage-workspace` is not smart about `publish = false`
### Problem

I was hoping to include `-Zpackage-workspace` as part of a workspace's CI so that it doesn't repeat certain embarrassing mistakes, but it didn't work as expected with respect to certain crates that have `publish = false` on them.

### Steps

After bumping the versions of all packages I wished to publish a new release for, I used this on the workspace with the following command:
```console
$ cargo +nightly package --workspace -Zpackage-workspace
     Locking 7 packages to latest compatible versions
    Updating cargo-pgrx v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/cargo-pgrx) -> v0.12.0-beta.4
    Updating pgrx v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx) -> v0.12.0-beta.4
    Updating pgrx-macros v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx-macros) -> v0.12.0-beta.4
    Updating pgrx-pg-config v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx-pg-config) -> v0.12.0-beta.4
    Updating pgrx-pg-sys v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx-pg-sys) -> v0.12.0-beta.4
    Updating pgrx-sql-entity-graph v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx-sql-entity-graph) -> v0.12.0-beta.4
    Updating pgrx-tests v0.12.0-beta.3 (/home/jubilee/tcdi/pgrx/pgrx-tests) -> v0.12.0-beta.4
error: `aggregate` cannot be packaged.
The registry `crates-io` is not listed in the `package.publish` value in Cargo.toml.
```

### Possible Solution(s)

This seems a curious error, given that this is intentional? I wish to not publish that crate and thus do not wish to package it for publishing?

Cargo has the option of doing the "smart" thing ("What I mean") here and simply ignoring such packages. The other obvious alternative is that it can simply continue to fail-fast, but if so it should probably have a more helpful error message. Note these are "leaf" packages on which nothing depends, so automatically excluding them _would_ work.

### Notes

I do have the option of simply removing it from the workspace, but it all gets a bit awkward. To complicate matters further, these packages being in the workspace does affect feature resolution which also affects whether or not basic `cargo check` or `cargo test` work (as the library is one of those "you must enable one and only one of these features" sorts).

Related:
- https://github.com/rust-lang/cargo/issues/10948

### Version

```text
cargo 1.82.0-nightly (b5d44db1d 2024-07-26)
release: 1.82.0-nightly
commit-hash: b5d44db1daf0469b227a6211b987162a39a54730
commit-date: 2024-07-26
host: x86_64-unknown-linux-gnu
libgit2: 1.8.1 (sys:0.19.0 vendored)
libcurl: 8.8.0-DEV (sys:0.4.73+curl-8.8.0 vendored ssl:OpenSSL/3.3.1)
ssl: OpenSSL 3.3.1 4 Jun 2024
os: Arch Linux Rolling Release [64-bit]
```


Pull request description:
Be more permissive while packaging unpublishable crates.
<!-- homu-ignore:start -->
<!--
Thanks for submitting a pull request 🎉! Here are some tips for you:

* If this is your first contribution, read "Cargo Contribution Guide" first:
  https://doc.crates.io/contrib/
* Run `cargo fmt --all` to format your code changes.
* Small commits and pull requests are always preferable and easy to review.
* If your idea is large and needs feedback from the community, read how:
  https://doc.crates.io/contrib/process/#working-on-large-features
* Cargo takes care of compatibility. Read our design principles:
  https://doc.crates.io/contrib/design.html
* When changing help text of cargo commands, follow the steps to generate docs:
  https://github.com/rust-lang/cargo/tree/master/src/doc#building-the-man-pages
* If your PR is not finished, set it as "draft" PR or add "WIP" in its title.
* It's ok to use the CI resources to test your PR, but please don't abuse them.

### What does this PR try to resolve?

Explain the motivation behind this change.
A clear overview along with an in-depth explanation are helpful.

You can use `Fixes #<issue number>` to associate this PR to an existing issue.

### How should we test and review this PR?

Demonstrate how you test this change and guide reviewers through your PR.
With a smooth review process, a pull request usually gets reviewed quicker.

If you don't know how to write and run your tests, please read the guide:
https://doc.crates.io/contrib/tests

### Additional information

Other information you want to mention in this PR, such as prior arts,
future extensions, an unresolved problem, or a TODO list.
-->
<!-- homu-ignore:end -->

This PR allows for packaging workspaces that include `publish = false` crates, in some circumstances:

- unpublishable crates are ignored when inferring the publish registry
- when checking whether the publish registry is valid, we ignore unpublishable crates
- we don't put unpublishable crates in the local registry overlay, so if any workspace crates depend on an unpublishable crate then they will fail to verify.

This PR also contains a refactor, moving the registry inference logic to `registry/mod.rs`, where it will be reused by the upcoming publish-workspace feature. I put the refactor and the logic changes in different commits.

Fixes #14356


Patch:
diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs
index 17ef0ecbb..aa5aca3c9 100644
--- a/src/cargo/ops/cargo_package.rs
+++ b/src/cargo/ops/cargo_package.rs
@@ -1,4 +1,4 @@
-use std::collections::{BTreeSet, HashMap, HashSet};
+use std::collections::{BTreeSet, HashMap};
 use std::fs::{self, File};
 use std::io::prelude::*;
 use std::io::SeekFrom;
@@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use std::task::Poll;
 
-use super::RegistryOrIndex;
 use crate::core::compiler::{BuildConfig, CompileMode, DefaultExecutor, Executor};
 use crate::core::dependency::DepKind;
 use crate::core::manifest::Target;
@@ -15,8 +14,9 @@ use crate::core::resolver::HasDevUnits;
 use crate::core::{Feature, PackageIdSpecQuery, Shell, Verbosity, Workspace};
 use crate::core::{Package, PackageId, PackageSet, Resolve, SourceId};
 use crate::ops::lockfile::LOCKFILE_NAME;
+use crate::ops::registry::{infer_registry, RegistryOrIndex};
 use crate::sources::registry::index::{IndexPackage, RegistryDependency};
-use crate::sources::{PathSource, SourceConfigMap, CRATES_IO_REGISTRY};
+use crate::sources::{PathSource, CRATES_IO_REGISTRY};
 use crate::util::cache_lock::CacheLockMode;
 use crate::util::context::JobsConfig;
 use crate::util::errors::CargoResult;
@@ -174,97 +174,6 @@ fn create_package(
     return Ok(dst);
 }
 
-/// Determine which registry the packages are for.
-///
-/// The registry only affects the built packages if there are dependencies within the
-/// packages that we're packaging: if we're packaging foo-bin and foo-lib, and foo-bin
-/// depends on foo-lib, then the foo-lib entry in foo-bin's lockfile will depend on the
-/// registry that we're building packages for.
-fn infer_registry(
-    gctx: &GlobalContext,
-    pkgs: &[&Package],
-    reg_or_index: Option<RegistryOrIndex>,
-) -> CargoResult<SourceId> {
-    let reg_or_index = match reg_or_index {
-        Some(r) => r,
-        None => {
-            if pkgs[1..].iter().all(|p| p.publish() == pkgs[0].publish()) {
-                // If all packages have the same publish settings, we take that as the default.
-                match pkgs[0].publish().as_deref() {
-                    Some([unique_pkg_reg]) => RegistryOrIndex::Registry(unique_pkg_reg.to_owned()),
-                    None | Some([]) => RegistryOrIndex::Registry(CRATES_IO_REGISTRY.to_owned()),
-                    Some([reg, ..]) if pkgs.len() == 1 => {
-                        // For backwards compatibility, avoid erroring if there's only one package.
-                        // The registry doesn't affect packaging in this case.
-                        RegistryOrIndex::Registry(reg.to_owned())
-                    }
-                    Some(regs) => {
-                        let mut regs: Vec<_> = regs.iter().map(|s| format!("\"{}\"", s)).collect();
-                        regs.sort();
-                        regs.dedup();
-                        // unwrap: the match block ensures that there's more than one reg.
-                        let (last_reg, regs) = regs.split_last().unwrap();
-                        bail!(
-                            "--registry is required to disambiguate between {} or {} registries",
-                            regs.join(", "),
-                            last_reg
-                        )
-                    }
-                }
-            } else {
-                let common_regs = pkgs
-                    .iter()
-                    // `None` means "all registries", so drop them instead of including them
-                    // in the intersection.
-                    .filter_map(|p| p.publish().as_deref())
-                    .map(|p| p.iter().collect::<HashSet<_>>())
-                    .reduce(|xs, ys| xs.intersection(&ys).cloned().collect())
-                    .unwrap_or_default();
-                if common_regs.is_empty() {
-                    bail!("conflicts between `package.publish` fields in the selected packages");
-                } else {
-                    bail!(
-                        "--registry is required because not all `package.publish` settings agree",
-                    );
-                }
-            }
-        }
-    };
-
-    // Validate the registry against the packages' allow-lists. For backwards compatibility, we
-    // skip this if only a single package is being published (because in that case the registry
-    // doesn't affect the packaging step).
-    if pkgs.len() > 1 {
-        if let RegistryOrIndex::Registry(reg_name) = &reg_or_index {
-            for pkg in pkgs {
-                if let Some(allowed) = pkg.publish().as_ref() {
-                    if !allowed.iter().any(|a| a == reg_name) {
-                        bail!(
-                        "`{}` cannot be packaged.\n\
-                         The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
-                        pkg.name(),
-                        reg_name
-                    );
-                    }
-                }
-            }
-        }
-    }
-
-    let sid = match reg_or_index {
-        RegistryOrIndex::Index(url) => SourceId::for_registry(&url)?,
-        RegistryOrIndex::Registry(reg) if reg == CRATES_IO_REGISTRY => SourceId::crates_io(gctx)?,
-        RegistryOrIndex::Registry(reg) => SourceId::alt_registry(gctx, &reg)?,
-    };
-
-    // Load source replacements that are built-in to Cargo.
-    let sid = SourceConfigMap::empty(gctx)?
-        .load(sid, &HashSet::new())?
-        .replaced_source_id();
-
-    Ok(sid)
-}
-
 /// Packages an entire workspace.
 ///
 /// Returns the generated package files. If `opts.list` is true, skips
@@ -293,19 +202,28 @@ pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<Fi
         // below, and will be validated during the verification step.
     }
 
+    let deps = local_deps(pkgs.iter().map(|(p, f)| ((*p).clone(), f.clone())));
     let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
-    let publish_reg = infer_registry(ws.gctx(), &just_pkgs, opts.reg_or_index.clone())?;
-    debug!("packaging for registry {publish_reg}");
+
+    // The publish registry doesn't matter unless there are local dependencies,
+    // so only try to get one if we need it. If they explicitly passed a
+    // registry on the CLI, we check it no matter what.
+    let sid = if deps.has_no_dependencies() && opts.reg_or_index.is_none() {
+        None
+    } else {
+        let sid = get_registry(ws.gctx(), &just_pkgs, opts.reg_or_index.clone())?;
+        debug!("packaging for registry {}", sid);
+        Some(sid)
+    };
 
     let mut local_reg = if ws.gctx().cli_unstable().package_workspace {
         let reg_dir = ws.target_dir().join("package").join("tmp-registry");
-        Some(TmpRegistry::new(ws.gctx(), reg_dir, publish_reg)?)
+        sid.map(|sid| TmpRegistry::new(ws.gctx(), reg_dir, sid))
+            .transpose()?
     } else {
         None
     };
 
-    let deps = local_deps(pkgs.iter().map(|(p, f)| ((*p).clone(), f.clone())));
-
     // Packages need to be created in dependency order, because dependencies must
     // be added to our local overlay before we can create lockfiles that depend on them.
     let sorted_pkgs = deps.sort();
@@ -325,7 +243,9 @@ pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<Fi
         } else {
             let tarball = create_package(ws, &pkg, ar_files, local_reg.as_ref())?;
             if let Some(local_reg) = local_reg.as_mut() {
-                local_reg.add_package(ws, &pkg, &tarball)?;
+                if pkg.publish() != &Some(Vec::new()) {
+                    local_reg.add_package(ws, &pkg, &tarball)?;
+                }
             }
             outputs.push((pkg, opts, tarball));
         }
@@ -343,6 +263,46 @@ pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<Fi
     Ok(outputs.into_iter().map(|x| x.2).collect())
 }
 
+/// Determine which registry the packages are for.
+///
+/// The registry only affects the built packages if there are dependencies within the
+/// packages that we're packaging: if we're packaging foo-bin and foo-lib, and foo-bin
+/// depends on foo-lib, then the foo-lib entry in foo-bin's lockfile will depend on the
+/// registry that we're building packages for.
+fn get_registry(
+    gctx: &GlobalContext,
+    pkgs: &[&Package],
+    reg_or_index: Option<RegistryOrIndex>,
+) -> CargoResult<SourceId> {
+    let reg_or_index = match reg_or_index.clone() {
+        Some(r) => Some(r),
+        None => infer_registry(pkgs)?,
+    };
+
+    // Validate the registry against the packages' allow-lists.
+    let reg = reg_or_index
+        .clone()
+        .unwrap_or_else(|| RegistryOrIndex::Registry(CRATES_IO_REGISTRY.to_owned()));
+    if let RegistryOrIndex::Registry(reg_name) = reg {
+        for pkg in pkgs {
+            if let Some(allowed) = pkg.publish().as_ref() {
+                // If allowed is empty (i.e. package.publish is false), we let it slide.
+                // This allows packaging unpublishable packages (although packaging might
+                // fail later if the unpublishable package is a dependency of something else).
+                if !allowed.is_empty() && !allowed.iter().any(|a| a == &reg_name) {
+                    bail!(
+                        "`{}` cannot be packaged.\n\
+                         The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
+                        pkg.name(),
+                        reg_name
+                    );
+                }
+            }
+        }
+    }
+    Ok(ops::registry::get_source_id(gctx, reg_or_index.as_ref())?.replacement)
+}
+
 /// Just the part of the dependency graph that's between the packages we're packaging.
 /// (Is the package name a good key? Does it uniquely identify packages?)
 #[derive(Clone, Debug, Default)]
@@ -359,6 +319,12 @@ impl LocalDependencies {
             .map(|name| self.packages[&name].clone())
             .collect()
     }
+
+    pub fn has_no_dependencies(&self) -> bool {
+        self.graph
+            .iter()
+            .all(|node| self.graph.edges(node).next().is_none())
+    }
 }
 
 /// Build just the part of the dependency graph that's between the given packages,
diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs
index 016d4cebb..93b48b6f6 100644
--- a/src/cargo/ops/registry/mod.rs
+++ b/src/cargo/ops/registry/mod.rs
@@ -19,7 +19,7 @@ use cargo_credential::{Operation, Secret};
 use crates_io::Registry;
 use url::Url;
 
-use crate::core::{PackageId, SourceId};
+use crate::core::{Package, PackageId, SourceId};
 use crate::sources::source::Source;
 use crate::sources::{RegistrySource, SourceConfigMap};
 use crate::util::auth;
@@ -191,7 +191,7 @@ fn registry(
 ///
 /// The return value is a pair of `SourceId`s: The first may be a built-in replacement of
 /// crates.io (such as index.crates.io), while the second is always the original source.
-fn get_source_id(
+pub(crate) fn get_source_id(
     gctx: &GlobalContext,
     reg_or_index: Option<&RegistryOrIndex>,
 ) -> CargoResult<RegistrySourceIds> {
@@ -322,3 +322,52 @@ pub(crate) struct RegistrySourceIds {
     /// User-defined source replacement is not applied.
     pub(crate) replacement: SourceId,
 }
+
+/// If this set of packages has an unambiguous publish registry, find it.
+pub(crate) fn infer_registry(pkgs: &[&Package]) -> CargoResult<Option<RegistryOrIndex>> {
+    // Ignore "publish = false" packages while inferring the registry.
+    let publishable_pkgs: Vec<_> = pkgs
+        .iter()
+        .filter(|p| p.publish() != &Some(Vec::new()))
+        .collect();
+
+    let Some((first, rest)) = publishable_pkgs.split_first() else {
+        return Ok(None);
+    };
+
+    // If all packages have the same publish settings, we take that as the default.
+    if rest.iter().all(|p| p.publish() == first.publish()) {
+        match publishable_pkgs[0].publish().as_deref() {
+            Some([unique_pkg_reg]) => {
+                Ok(Some(RegistryOrIndex::Registry(unique_pkg_reg.to_owned())))
+            }
+            None | Some([]) => Ok(None),
+            Some(regs) => {
+                let mut regs: Vec<_> = regs.iter().map(|s| format!("\"{}\"", s)).collect();
+                regs.sort();
+                regs.dedup();
+                // unwrap: the match block ensures that there's more than one reg.
+                let (last_reg, regs) = regs.split_last().unwrap();
+                bail!(
+                    "--registry is required to disambiguate between {} or {} registries",
+                    regs.join(", "),
+                    last_reg
+                )
+            }
+        }
+    } else {
+        let common_regs = publishable_pkgs
+            .iter()
+            // `None` means "all registries", so drop them instead of including them
+            // in the intersection.
+            .filter_map(|p| p.publish().as_deref())
+            .map(|p| p.iter().collect::<HashSet<_>>())
+            .reduce(|xs, ys| xs.intersection(&ys).cloned().collect())
+            .unwrap_or_default();
+        if common_regs.is_empty() {
+            bail!("conflicts between `package.publish` fields in the selected packages");
+        } else {
+            bail!("--registry is required because not all `package.publish` settings agree",);
+        }
+    }
+}
diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md
index ce7f2cb6d..339fd99f4 100644
--- a/src/doc/src/reference/unstable.md
+++ b/src/doc/src/reference/unstable.md
@@ -115,7 +115,6 @@ Each new feature described below should explain how to use it.
 * Other
     * [gitoxide](#gitoxide) --- Use `gitoxide` instead of `git2` for a set of operations.
     * [script](#script) --- Enable support for single-file `.rs` packages.
-    * [lockfile-path](#lockfile-path) --- Allows to specify a path to lockfile other than the default path `<workspace_root>/Cargo.lock`.
 
 ## allow-features
 
@@ -1626,23 +1625,6 @@ will prefer the value in the configuration. The allows Cargo to add new built-in
 path bases without compatibility issues (as existing uses will shadow the
 built-in name).
 
-## lockfile-path
-* Original Issue: [#5707](https://github.com/rust-lang/cargo/issues/5707)
-* Tracking Issue: [#14421](https://github.com/rust-lang/cargo/issues/14421)
-
-This feature allows you to specify the path of lockfile Cargo.lock. 
-By default, lockfile is written into `<workspace_root>/Cargo.lock`. 
-However, when sources are stored in read-only directory, most of the cargo commands 
-would fail, trying to write a lockfile. The `--lockfile-path`
-flag makes it easier to work with readonly sources. 
-Note, that currently path must end with `Cargo.lock`. Meaning, if you want to use 
-this feature in multiple projects, lockfiles should be stored in different directories.
-Example:
-
-```sh
-cargo +nightly metadata --lockfile-path=$LOCKFILES_ROOT/my-project/Cargo.lock -Z unstable-options
-```
-
 # Stabilized and removed features
 
 ## Compile progress
diff --git a/triagebot.toml b/triagebot.toml
index 2a10ae976..609d76830 100644
--- a/triagebot.toml
+++ b/triagebot.toml
@@ -270,9 +270,6 @@ trigger_files = ["src/bin/cargo/commands/generate_lockfile.rs"]
 [autolabel."Command-git-checkout"]
 trigger_files = ["src/bin/cargo/commands/git_checkout.rs"]
 
-[autolabel."Command-info"]
-trigger_files = ["src/bin/cargo/commands/info.rs", "src/cargo/ops/registry/info/"]
-
 [autolabel."Command-init"]
 trigger_files = ["src/bin/cargo/commands/init.rs"]
 


Test patch:
diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs
index 799540623..7b937ca53 100644
--- a/tests/testsuite/package.rs
+++ b/tests/testsuite/package.rs
@@ -5999,7 +5999,8 @@ fn registry_not_in_publish_list() {
         .masquerade_as_nightly_cargo(&["package-workspace"])
         .with_status(101)
         .with_stderr_data(str![[r#"
-[ERROR] registry index was not found in any configuration: `alternative`
+[ERROR] `foo` cannot be packaged.
+The registry `alternative` is not listed in the `package.publish` value in Cargo.toml.
 
 "#]])
         .run();
@@ -6173,6 +6174,99 @@ The registry `alternative` is not listed in the `package.publish` value in Cargo
     .run();
 }
 
+#[cargo_test]
+fn registry_inference_ignores_unpublishable() {
+    let _alt_reg = registry::RegistryBuilder::new()
+        .http_api()
+        .http_index()
+        .alternative()
+        .build();
+
+    let p = project()
+        .file(
+            "Cargo.toml",
+            r#"
+            [workspace]
+            members = ["dep", "main"]
+            "#,
+        )
+        .file(
+            "main/Cargo.toml",
+            r#"
+            [package]
+            name = "main"
+            version = "0.0.1"
+            edition = "2015"
+            authors = []
+            license = "MIT"
+            description = "main"
+            repository = "bar"
+            publish = false
+
+            [dependencies]
+            dep = { path = "../dep", version = "0.1.0", registry = "alternative" }
+        "#,
+        )
+        .file("main/src/main.rs", "fn main() {}")
+        .file(
+            "dep/Cargo.toml",
+            r#"
+            [package]
+            name = "dep"
+            version = "0.1.0"
+            edition = "2015"
+            authors = []
+            license = "MIT"
+            description = "dep"
+            repository = "bar"
+            publish = ["alternative"]
+        "#,
+        )
+        .file("dep/src/lib.rs", "")
+        .build();
+
+    p.cargo("package -Zpackage-workspace")
+        .masquerade_as_nightly_cargo(&["package-workspace"])
+        .with_stderr_data(str![[r#"
+[PACKAGING] dep v0.1.0 ([ROOT]/foo/dep)
+[PACKAGED] 3 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
+[PACKAGING] main v0.0.1 ([ROOT]/foo/main)
+[UPDATING] `alternative` index
+[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
+[VERIFYING] dep v0.1.0 ([ROOT]/foo/dep)
+[COMPILING] dep v0.1.0 ([ROOT]/foo/target/package/dep-0.1.0)
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+[VERIFYING] main v0.0.1 ([ROOT]/foo/main)
+[UPDATING] `alternative` index
+[UNPACKING] dep v0.1.0 (registry `[ROOT]/foo/target/package/tmp-registry`)
+[COMPILING] dep v0.1.0 (registry `alternative`)
+[COMPILING] main v0.0.1 ([ROOT]/foo/target/package/main-0.0.1)
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+        .run();
+
+    p.cargo("package -Zpackage-workspace --registry=alternative")
+        .masquerade_as_nightly_cargo(&["package-workspace"])
+        .with_stderr_data(str![[r#"
+[PACKAGING] dep v0.1.0 ([ROOT]/foo/dep)
+[PACKAGED] 3 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
+[PACKAGING] main v0.0.1 ([ROOT]/foo/main)
+[UPDATING] `alternative` index
+[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
+[VERIFYING] dep v0.1.0 ([ROOT]/foo/dep)
+[COMPILING] dep v0.1.0 ([ROOT]/foo/target/package/dep-0.1.0)
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+[VERIFYING] main v0.0.1 ([ROOT]/foo/main)
+[UPDATING] `alternative` index
+[COMPILING] dep v0.1.0 (registry `alternative`)
+[COMPILING] main v0.0.1 ([ROOT]/foo/target/package/main-0.0.1)
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+        .run();
+}
+
 #[cargo_test]
 fn registry_not_inferred_because_of_multiple_options() {
     let _alt_reg = registry::RegistryBuilder::new()
@@ -6337,3 +6431,72 @@ fn registry_not_inferred_because_of_mismatch() {
 "#]])
         .run();
 }
+
+#[cargo_test]
+fn unpublishable_dependency() {
+    let _alt_reg = registry::RegistryBuilder::new()
+        .http_api()
+        .http_index()
+        .alternative()
+        .build();
+
+    let p = project()
+        .file(
+            "Cargo.toml",
+            r#"
+            [workspace]
+            members = ["dep", "main"]
+            "#,
+        )
+        .file(
+            "main/Cargo.toml",
+            r#"
+            [package]
+            name = "main"
+            version = "0.0.1"
+            edition = "2015"
+            authors = []
+            license = "MIT"
+            description = "main"
+            repository = "bar"
+
+            [dependencies]
+            dep = { path = "../dep", version = "0.1.0", registry = "alternative" }
+        "#,
+        )
+        .file("main/src/main.rs", "fn main() {}")
+        .file(
+            "dep/Cargo.toml",
+            r#"
+            [package]
+            name = "dep"
+            version = "0.1.0"
+            edition = "2015"
+            authors = []
+            license = "MIT"
+            description = "dep"
+            repository = "bar"
+            publish = false
+        "#,
+        )
+        .file("dep/src/lib.rs", "")
+        .build();
+
+    p.cargo("package -Zpackage-workspace")
+        .masquerade_as_nightly_cargo(&["package-workspace"])
+        .with_status(101)
+        .with_stderr_data(str![[r#"
+[PACKAGING] dep v0.1.0 ([ROOT]/foo/dep)
+[PACKAGED] 3 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
+[PACKAGING] main v0.0.1 ([ROOT]/foo/main)
+[UPDATING] `alternative` index
+[ERROR] failed to prepare local package for uploading
+
+Caused by:
+  no matching package named `dep` found
+  location searched: registry `alternative`
+  required by package `main v0.0.1 ([ROOT]/foo/main)`
+
+"#]])
+        .run();
+}