Repository: rust-lang/cargo (rust)

Issue:
Cargo 1.78+ incorrectly drops `FeatureValue`s that match the implicit feature
### Problem

Cargo 1.78 and 1.79 drops `FeatureValue`s that match the default feature, resulting in an invalid feature map that later causes Cargo to ignore the version when parsing the index in the slow path of `Summaries::parse()` due to `build_feature_map()` returning an error.

__Example error message__:

```
   0.378811004s TRACE cargo::sources::registry::index: json parsed registry hs-clarity-resource-locator-rust/4.2.4
   0.381279937s  INFO cargo::sources::registry::index: failed to parse "hs/-c/hs-clarity-resource-locator-rust" registry package: optional dependency `tonic_010` is not included in any feature
Make sure that `dep:tonic_010` is included in one of features in the [features] table.
```

The corresponding `Cargo.toml` looks like this:

```toml
[package]
name = "hs-clarity-resource-locator-rust"
version = "0.1.0"
edition = "2021"

[features]
tonic_010 = ["dep:tonic_010"]

[dependencies]
tonic_010 = { package = "tonic", version = "0.10", optional = true }
```

❌ The error occurs because when this crate is published by Cargo 1.78 or 1.79, the feature map is broken:

```json5
{
  // ...
  "features": {
    "tonic_010": []
  }
}
```

✅ When published with Cargo 1.76 or 1.77 instead, you get:

```json5
{
  // ...
  "features": {
    "tonic_010": [
      "dep:tonic_010"
    ]
  }
}
```

✅ When removing the `[features]` table from the `Cargo.toml`, you get:

```json5
{
  // ...
  "features": {}
  }
}
```

My hypothesis is that Cargo does some "cleaning" of the features map, maybe attempting to remove features that would be implied anyway, but it results in a broken map.


### Steps

1. Create a new crate with a `Cargo.toml` akin to the one shown above
2. Publish it to a registry
3. Inspect the published version's feature map

More elaborate local testing:

1. Step (1) from above
3. Run the `sparsereg.py` from below to record a `newcrate.json` when publishing
5. Publish the crate to `sparse+http://localhost:8000/cargo/foorepo/index/`
6. Apply `0001-Add-cargo-build-feature-map.rs.patch` 
7. `cargo run --bin cargo-build-feature-map -- newcrate.json`

__Result__

```
❯ cargo run --bin cargo-build-feature-map -- ~/Desktop/newcrate.json 
   Compiling cargo v0.83.0 (/home/coder/git/rust-lang/cargo)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.43s
     Running `target/debug/cargo-build-feature-map /home/coder/git/niklas/Desktop/newcrate.json`
{"name":"hs-clarity-resource-locator-rust","vers":"0.1.0","deps":[{"name":"tonic_010","optional":true,"explicit_name_in_toml":null}],"features":{"tonic_010":[]}}
Error: optional dependency `tonic_010` is not included in any feature
Make sure that `dep:tonic_010` is included in one of features in the [features] table.
```


__Additional files__

<details><summary><code>sparsereg.py</code></summary>

```py
"""
Implements the basic API endpoints for a Cargo sparse registry to perform a `cargo publish`.
Records the received crate's metadata in a `newcrate.json` file in the CWD.

Dependencies:

- fastapi

Run:

    $ fastapi run sparsereg.py
"""

import struct
from typing import Any
from fastapi import FastAPI, Request


app = FastAPI()


@app.get("/cargo/{repository}/index/config.json")
def config_json(repository: str) -> dict[str, str]:
    return {
        "dl": "http://localhost:8000/cargo/v1/crates",
        "api": f"http://localhost:8000/cargo/{repository}",
    }


@app.put("/cargo/{repository}/api/v1/crates/new")
async def new_crate(repository: str, request: Request) -> dict[str, Any]:
    body = await request.body()
    length = struct.unpack("I", body[:4])[0]
    with open("newcrate.json", "wb") as fp:
        fp.write(body[4 : length + 4])
    return {}

```

</details>

<details><summary><code>0001-Add-cargo-build-feature-map.rs.patch</code></summary>

```patch
From 1169b45dcb59e503f980b926bda9a89b3fad51e2 Mon Sep 17 00:00:00 2001
From: Niklas Rosenstein <rosensteinniklas@gmail.com>
Date: Tue, 30 Jul 2024 07:04:27 +0000
Subject: [PATCH 1/1] Add cargo-build-feature-map.rs

---
 Cargo.toml                         |  5 ++
 src/bin/cargo-build-feature-map.rs | 77 ++++++++++++++++++++++++++++++
 src/cargo/core/summary.rs          |  2 +-
 3 files changed, 83 insertions(+), 1 deletion(-)
 create mode 100644 src/bin/cargo-build-feature-map.rs

diff --git a/Cargo.toml b/Cargo.toml
index 77e7f9092..c9cf20eb4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -251,6 +251,11 @@ name = "cargo"
 test = false
 doc = false
 
+[[bin]]
+name = "cargo-build-feature-map"
+test = false
+doc = false
+
 [features]
 vendored-openssl = ["openssl/vendored"]
 vendored-libgit2 = ["libgit2-sys/vendored"]
diff --git a/src/bin/cargo-build-feature-map.rs b/src/bin/cargo-build-feature-map.rs
new file mode 100644
index 000000000..48e9506a4
--- /dev/null
+++ b/src/bin/cargo-build-feature-map.rs
@@ -0,0 +1,77 @@
+use std::collections::BTreeMap;
+use std::error::Error;
+
+use cargo::core::summary::build_feature_map;
+use cargo::util::interning::InternedString;
+use itertools::Itertools;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Deserialize, Serialize)]
+struct PackageDep {
+    name: String,
+    optional: Option<bool>,
+
+    // For metadata from the Publish API, this contains the actual name in the TOML file (the alias).
+    // In the Sparse Registry Index API, the `name` field is already the actual name in the TOML.
+    explicit_name_in_toml: Option<String>,
+}
+
+impl PackageDep {
+    fn normalize(self) -> PackageDep {
+        if let Some(ref toml_name) = self.explicit_name_in_toml {
+            PackageDep {
+                name: toml_name.clone(),
+                optional: self.optional,
+                explicit_name_in_toml: None,
+            }
+        } else {
+            self.clone()
+        }
+    }
+}
+
+impl Into<cargo::core::Dependency> for PackageDep {
+    fn into(self) -> cargo::core::Dependency {
+        // Fake source ID, irrelevant.
+        let source_id = cargo::core::SourceId::from_url("git+https://foobar").unwrap();
+        let mut result =
+            cargo::core::Dependency::new_override(InternedString::new(&self.name), source_id);
+        result.set_optional(self.optional.unwrap_or(false));
+        result
+    }
+}
+
+#[derive(Deserialize, Serialize)]
+struct Package {
+    name: String,
+    vers: String,
+    deps: Vec<PackageDep>,
+    features: BTreeMap<InternedString, Vec<InternedString>>,
+}
+
+pub fn main() -> Result<(), Box<dyn Error>> {
+    let cmd = clap::Command::new("cargo-build-feature-map").args([clap::Arg::new("file")]);
+    let args = cmd.get_matches();
+
+    let file = args.get_one::<String>("file").unwrap();
+    let mut pkg: Package = serde_json::from_str(&std::fs::read_to_string(&file)?)?;
+
+    // Adjust for asymmetry between Registry API and Sparse registry API.
+    pkg.deps = pkg
+        .deps
+        .into_iter()
+        .map(PackageDep::normalize)
+        .collect_vec();
+
+    // Re-serialize the content. Makes it easier to compare one file with the other later.
+    println!("{}", serde_json::to_string(&pkg)?);
+
+    // Validates the feature map.
+    let feature_map = build_feature_map(
+        &pkg.features,
+        &pkg.deps.into_iter().map(Into::into).collect_vec(),
+    )?;
+    println!("{:?}", feature_map);
+
+    Ok(())
+}
diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs
index 424328fa6..05ff9f33c 100644
--- a/src/cargo/core/summary.rs
+++ b/src/cargo/core/summary.rs
@@ -186,7 +186,7 @@ const _: fn() = || {
 
 /// Checks features for errors, bailing out a CargoResult:Err if invalid,
 /// and creates FeatureValues for each feature.
-fn build_feature_map(
+pub fn build_feature_map(
     features: &BTreeMap<InternedString, Vec<InternedString>>,
     dependencies: &[Dependency],
 ) -> CargoResult<FeatureMap> {
-- 
2.45.2
```

</details>



### Possible Solution(s)

_No response_

### Notes

_No response_

### Version

```text
cargo 1.79.0 (ffa9cf99a 2024-06-03)
release: 1.79.0
commit-hash: ffa9cf99a594e59032757403d4c780b46dc2c43a
commit-date: 2024-06-03
host: x86_64-unknown-linux-gnu
libgit2: 1.7.2 (sys:0.18.3 vendored)
libcurl: 8.6.0-DEV (sys:0.4.72+curl-8.6.0 vendored ssl:OpenSSL/1.1.1w)
ssl: OpenSSL 1.1.1w  11 Sep 2023
os: Ubuntu 22.4.0 (jammy) [64-bit]
```



Pull request:
fix(publish): Don't strip non-dev features
### What does this PR try to resolve?

First, we added support for stripping of local-only dev-dependencies.
This was dual-implemented for `Cargo.toml` and `Summary`.

This left off stripping of `dep/feature` that reference dev-dependencies
(enabling features within dev-dependencies).
When we fixed this, we again dual-implemented it.

The `Cargo.toml` version was correct but the `Summary` version was
instead stripping too many features,
particularly features that reference renamed dependencies.
We didn't have tests for this case and it wasn't caught earlier because
crates.io re-generates the `Summary` from `Cargo.toml`, ignoring what we
post.
That makes this only show up with custom registries that trust what
Cargo posts.

Rather than fixing the `Summary` generation, I remove the
dual-implementation and instead generate the `Summary` from the
published `Cargo.toml`.

Fixes #14321

### How should we test and review this PR?


### Additional information

Unfortunately, we don't have access directly to the packaged
`Cargo.toml`.
It could be passed around and I originally did so, hoping to remove use
of the local `Package`.
However, the local `Package` is needed for things like reading the
`README`.
So I scaled back and isolate the change to only what needs it.
This also makes it easier for `prepare_transmit` callers.
Fully open to someone exploring removing this extra `prepare_for_publish` in the future.


Patch:
diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs
index a83df435b..459e780f3 100644
--- a/src/cargo/ops/cargo_package.rs
+++ b/src/cargo/ops/cargo_package.rs
@@ -288,7 +288,7 @@ 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(&pkg, &tarball)?;
+                local_reg.add_package(ws, &pkg, &tarball)?;
             }
             outputs.push((pkg, opts, tarball));
         }
@@ -893,7 +893,7 @@ fn tar(
         .iter()
         .map(|ar_file| ar_file.rel_path.clone())
         .collect::<Vec<_>>();
-    let publish_pkg = prepare_for_publish(pkg, ws, &included)?;
+    let publish_pkg = prepare_for_publish(pkg, ws, Some(&included))?;
 
     let mut uncompressed_size = 0;
     for ar_file in ar_files {
@@ -1299,7 +1299,12 @@ impl<'a> TmpRegistry<'a> {
         self.root.join("index")
     }
 
-    fn add_package(&mut self, package: &Package, tar: &FileLock) -> CargoResult<()> {
+    fn add_package(
+        &mut self,
+        ws: &Workspace<'_>,
+        package: &Package,
+        tar: &FileLock,
+    ) -> CargoResult<()> {
         debug!(
             "adding package {}@{} to local overlay at {}",
             package.name(),
@@ -1317,7 +1322,7 @@ impl<'a> TmpRegistry<'a> {
             tar_copy.flush()?;
         }
 
-        let new_crate = super::registry::prepare_transmit(self.gctx, package, self.upstream)?;
+        let new_crate = super::registry::prepare_transmit(self.gctx, ws, package, self.upstream)?;
 
         tar.file().seek(SeekFrom::Start(0))?;
         let cksum = cargo_util::Sha256::new()
diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs
index f0a72bda4..592a1cb07 100644
--- a/src/cargo/ops/registry/publish.rs
+++ b/src/cargo/ops/registry/publish.rs
@@ -3,7 +3,6 @@
 //! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#publish
 
 use std::collections::BTreeMap;
-use std::collections::BTreeSet;
 use std::collections::HashSet;
 use std::fs::File;
 use std::time::Duration;
@@ -21,7 +20,6 @@ use crate::core::dependency::DepKind;
 use crate::core::manifest::ManifestMetadata;
 use crate::core::resolver::CliFeatures;
 use crate::core::Dependency;
-use crate::core::FeatureValue;
 use crate::core::Package;
 use crate::core::PackageIdSpecQuery;
 use crate::core::SourceId;
@@ -35,7 +33,7 @@ use crate::sources::CRATES_IO_REGISTRY;
 use crate::util::auth;
 use crate::util::cache_lock::CacheLockMode;
 use crate::util::context::JobsConfig;
-use crate::util::interning::InternedString;
+use crate::util::toml::prepare_for_publish;
 use crate::util::Progress;
 use crate::util::ProgressStyle;
 use crate::CargoResult;
@@ -185,6 +183,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
         .status("Uploading", pkg.package_id().to_string())?;
     transmit(
         opts.gctx,
+        ws,
         pkg,
         tarball.file(),
         &mut registry,
@@ -324,16 +323,16 @@ fn verify_dependencies(
 
 pub(crate) fn prepare_transmit(
     gctx: &GlobalContext,
-    pkg: &Package,
+    ws: &Workspace<'_>,
+    local_pkg: &Package,
     registry_id: SourceId,
 ) -> CargoResult<NewCrate> {
-    let deps = pkg
+    let included = None; // don't filter build-targets
+    let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
+
+    let deps = publish_pkg
         .dependencies()
         .iter()
-        .filter(|dep| {
-            // Skip dev-dependency without version.
-            dep.is_transitive() || dep.specified_req()
-        })
         .map(|dep| {
             // If the dependency is from a different registry, then include the
             // registry in the dependency.
@@ -378,7 +377,7 @@ pub(crate) fn prepare_transmit(
             })
         })
         .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
-    let manifest = pkg.manifest();
+    let manifest = publish_pkg.manifest();
     let ManifestMetadata {
         ref authors,
         ref description,
@@ -395,44 +394,30 @@ pub(crate) fn prepare_transmit(
         ref rust_version,
     } = *manifest.metadata();
     let rust_version = rust_version.as_ref().map(ToString::to_string);
-    let readme_content = readme
+    let readme_content = local_pkg
+        .manifest()
+        .metadata()
+        .readme
         .as_ref()
         .map(|readme| {
-            paths::read(&pkg.root().join(readme))
-                .with_context(|| format!("failed to read `readme` file for package `{}`", pkg))
+            paths::read(&local_pkg.root().join(readme)).with_context(|| {
+                format!("failed to read `readme` file for package `{}`", local_pkg)
+            })
         })
         .transpose()?;
-    if let Some(ref file) = *license_file {
-        if !pkg.root().join(file).exists() {
+    if let Some(ref file) = local_pkg.manifest().metadata().license_file {
+        if !local_pkg.root().join(file).exists() {
             bail!("the license file `{}` does not exist", file)
         }
     }
 
-    let deps_set = deps
-        .iter()
-        .map(|dep| dep.name.clone())
-        .collect::<BTreeSet<String>>();
-
     let string_features = match manifest.resolved_toml().features() {
         Some(features) => features
             .iter()
             .map(|(feat, values)| {
                 (
                     feat.to_string(),
-                    values
-                        .iter()
-                        .filter(|fv| {
-                            let feature_value = FeatureValue::new(InternedString::new(fv));
-                            match feature_value {
-                                FeatureValue::Dep { dep_name }
-                                | FeatureValue::DepFeature { dep_name, .. } => {
-                                    deps_set.contains(&dep_name.to_string())
-                                }
-                                _ => true,
-                            }
-                        })
-                        .map(|fv| fv.to_string())
-                        .collect(),
+                    values.iter().map(|fv| fv.to_string()).collect(),
                 )
             })
             .collect::<BTreeMap<String, Vec<String>>>(),
@@ -440,8 +425,8 @@ pub(crate) fn prepare_transmit(
     };
 
     Ok(NewCrate {
-        name: pkg.name().to_string(),
-        vers: pkg.version().to_string(),
+        name: publish_pkg.name().to_string(),
+        vers: publish_pkg.version().to_string(),
         deps,
         features: string_features,
         authors: authors.clone(),
@@ -463,13 +448,14 @@ pub(crate) fn prepare_transmit(
 
 fn transmit(
     gctx: &GlobalContext,
+    ws: &Workspace<'_>,
     pkg: &Package,
     tarball: &File,
     registry: &mut Registry,
     registry_id: SourceId,
     dry_run: bool,
 ) -> CargoResult<()> {
-    let new_crate = prepare_transmit(gctx, pkg, registry_id)?;
+    let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
 
     // Do not upload if performing a dry run
     if dry_run {
diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs
index 2fa704a8b..76892b9d7 100644
--- a/src/cargo/util/toml/mod.rs
+++ b/src/cargo/util/toml/mod.rs
@@ -2551,15 +2551,16 @@ fn unused_dep_keys(
     }
 }
 
+/// Make the [`Package`] self-contained so its ready for packaging
 pub fn prepare_for_publish(
     me: &Package,
     ws: &Workspace<'_>,
-    included: &[PathBuf],
+    packaged_files: Option<&[PathBuf]>,
 ) -> CargoResult<Package> {
     let contents = me.manifest().contents();
     let document = me.manifest().document();
     let original_toml =
-        prepare_toml_for_publish(me.manifest().resolved_toml(), ws, me.root(), included)?;
+        prepare_toml_for_publish(me.manifest().resolved_toml(), ws, me.root(), packaged_files)?;
     let resolved_toml = original_toml.clone();
     let features = me.manifest().unstable_features().clone();
     let workspace_config = me.manifest().workspace_config().clone();
@@ -2591,7 +2592,7 @@ fn prepare_toml_for_publish(
     me: &manifest::TomlManifest,
     ws: &Workspace<'_>,
     package_root: &Path,
-    included: &[PathBuf],
+    packaged_files: Option<&[PathBuf]>,
 ) -> CargoResult<manifest::TomlManifest> {
     let gctx = ws.gctx();
 
@@ -2608,7 +2609,8 @@ fn prepare_toml_for_publish(
     package.workspace = None;
     if let Some(StringOrBool::String(path)) = &package.build {
         let path = paths::normalize_path(Path::new(path));
-        let build = if included.contains(&path) {
+        let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true);
+        let build = if included {
             let path = path
                 .into_os_string()
                 .into_string()
@@ -2712,14 +2714,16 @@ fn prepare_toml_for_publish(
     }
 
     let lib = if let Some(target) = &me.lib {
-        prepare_target_for_publish(target, included, "library", ws.gctx())?
+        prepare_target_for_publish(target, packaged_files, "library", ws.gctx())?
     } else {
         None
     };
-    let bin = prepare_targets_for_publish(me.bin.as_ref(), included, "binary", ws.gctx())?;
-    let example = prepare_targets_for_publish(me.example.as_ref(), included, "example", ws.gctx())?;
-    let test = prepare_targets_for_publish(me.test.as_ref(), included, "test", ws.gctx())?;
-    let bench = prepare_targets_for_publish(me.bench.as_ref(), included, "benchmark", ws.gctx())?;
+    let bin = prepare_targets_for_publish(me.bin.as_ref(), packaged_files, "binary", ws.gctx())?;
+    let example =
+        prepare_targets_for_publish(me.example.as_ref(), packaged_files, "example", ws.gctx())?;
+    let test = prepare_targets_for_publish(me.test.as_ref(), packaged_files, "test", ws.gctx())?;
+    let bench =
+        prepare_targets_for_publish(me.bench.as_ref(), packaged_files, "benchmark", ws.gctx())?;
 
     let all = |_d: &manifest::TomlDependency| true;
     let mut manifest = manifest::TomlManifest {
@@ -2877,7 +2881,7 @@ fn prepare_toml_for_publish(
 
 fn prepare_targets_for_publish(
     targets: Option<&Vec<manifest::TomlTarget>>,
-    included: &[PathBuf],
+    packaged_files: Option<&[PathBuf]>,
     context: &str,
     gctx: &GlobalContext,
 ) -> CargoResult<Option<Vec<manifest::TomlTarget>>> {
@@ -2887,7 +2891,8 @@ fn prepare_targets_for_publish(
 
     let mut prepared = Vec::with_capacity(targets.len());
     for target in targets {
-        let Some(target) = prepare_target_for_publish(target, included, context, gctx)? else {
+        let Some(target) = prepare_target_for_publish(target, packaged_files, context, gctx)?
+        else {
             continue;
         };
         prepared.push(target);
@@ -2902,19 +2907,21 @@ fn prepare_targets_for_publish(
 
 fn prepare_target_for_publish(
     target: &manifest::TomlTarget,
-    included: &[PathBuf],
+    packaged_files: Option<&[PathBuf]>,
     context: &str,
     gctx: &GlobalContext,
 ) -> CargoResult<Option<manifest::TomlTarget>> {
     let path = target.path.as_ref().expect("previously resolved");
     let path = normalize_path(&path.0);
-    if !included.contains(&path) {
-        let name = target.name.as_ref().expect("previously resolved");
-        gctx.shell().warn(format!(
-            "ignoring {context} `{name}` as `{}` is not included in the published package",
-            path.display()
-        ))?;
-        return Ok(None);
+    if let Some(packaged_files) = packaged_files {
+        if !packaged_files.contains(&path) {
+            let name = target.name.as_ref().expect("previously resolved");
+            gctx.shell().warn(format!(
+                "ignoring {context} `{name}` as `{}` is not included in the published package",
+                path.display()
+            ))?;
+            return Ok(None);
+        }
     }
 
     let mut target = target.clone();


Test patch:
diff --git a/tests/testsuite/inheritable_workspace_fields.rs b/tests/testsuite/inheritable_workspace_fields.rs
index 70fb61197..664e25602 100644
--- a/tests/testsuite/inheritable_workspace_fields.rs
+++ b/tests/testsuite/inheritable_workspace_fields.rs
@@ -752,11 +752,11 @@ You may press ctrl-c to skip waiting; the crate should be available shortly.
           "homepage": "https://www.rust-lang.org",
           "keywords": ["cli"],
           "license": "MIT",
-          "license_file": "../LICENSE",
+          "license_file": "LICENSE",
           "links": null,
           "name": "bar",
           "readme": "README.md",
-          "readme_file": "../README.md",
+          "readme_file": "README.md",
           "repository": "https://github.com/example/example",
           "rust_version": "1.60",
           "vers": "1.2.3"
diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs
index c03e726d6..cb9765704 100644
--- a/tests/testsuite/publish.rs
+++ b/tests/testsuite/publish.rs
@@ -1598,112 +1598,20 @@ You may press ctrl-c to skip waiting; the crate should be available shortly.
 }
 
 #[cargo_test]
-fn publish_dev_dep_no_version() {
-    let registry = RegistryBuilder::new().http_api().http_index().build();
-
-    let p = project()
-        .file(
-            "Cargo.toml",
-            r#"
-            [package]
-            name = "foo"
-            version = "0.1.0"
-            edition = "2015"
-            authors = []
-            license = "MIT"
-            description = "foo"
-            documentation = "foo"
-            homepage = "foo"
-            repository = "foo"
-
-            [dev-dependencies]
-            bar = { path = "bar" }
-            "#,
-        )
-        .file("src/lib.rs", "")
-        .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
-        .file("bar/src/lib.rs", "")
-        .build();
-
-    p.cargo("publish --no-verify")
-        .replace_crates_io(registry.index_url())
-        .with_stderr_data(str![[r#"
-[UPDATING] crates.io index
-[PACKAGING] foo v0.1.0 ([ROOT]/foo)
-[PACKAGED] 3 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
-[UPLOADING] foo v0.1.0 ([ROOT]/foo)
-[UPLOADED] foo v0.1.0 to registry `crates-io`
-[NOTE] waiting for `foo v0.1.0` to be available at registry `crates-io`.
-You may press ctrl-c to skip waiting; the crate should be available shortly.
-[PUBLISHED] foo v0.1.0 at registry `crates-io`
-
-"#]])
-        .run();
-
-    publish::validate_upload_with_contents(
-        r#"
-        {
-          "authors": [],
-          "badges": {},
-          "categories": [],
-          "deps": [],
-          "description": "foo",
-          "documentation": "foo",
-          "features": {},
-          "homepage": "foo",
-          "keywords": [],
-          "license": "MIT",
-          "license_file": null,
-          "links": null,
-          "name": "foo",
-          "readme": null,
-          "readme_file": null,
-          "repository": "foo",
-          "rust_version": null,
-          "vers": "0.1.0"
-        }
-        "#,
-        "foo-0.1.0.crate",
-        &["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
-        &[(
-            "Cargo.toml",
-            &format!(
-                r#"{}
-[package]
-edition = "2015"
-name = "foo"
-version = "0.1.0"
-authors = []
-build = false
-autobins = false
-autoexamples = false
-autotests = false
-autobenches = false
-description = "foo"
-homepage = "foo"
-documentation = "foo"
-readme = false
-license = "MIT"
-repository = "foo"
-
-[lib]
-name = "foo"
-path = "src/lib.rs"
-
-[dev-dependencies]
-"#,
-                cargo::core::manifest::MANIFEST_PREAMBLE
-            ),
-        )],
-    );
-}
-
-#[cargo_test]
-fn publish_with_feature_point_diff_kinds_dep() {
+fn publish_dev_dep_stripping() {
     let registry = RegistryBuilder::new().http_api().http_index().build();
     Package::new("normal-only", "1.0.0")
         .feature("cat", &[])
         .publish();
+    Package::new("optional-dep-feature", "1.0.0")
+        .feature("cat", &[])
+        .publish();
+    Package::new("optional-namespaced", "1.0.0")
+        .feature("cat", &[])
+        .publish();
+    Package::new("optional-renamed-namespaced", "1.0.0")
+        .feature("cat", &[])
+        .publish();
     Package::new("build-only", "1.0.0")
         .feature("cat", &[])
         .publish();
@@ -1745,11 +1653,17 @@ fn publish_with_feature_point_diff_kinds_dep() {
                 "target-build-only/cat",
                 "target-dev-only/cat",
                 "target-normal-and-dev/cat",
+                "optional-dep-feature/cat",
+                "dep:optional-namespaced",
+                "dep:optional-renamed-namespaced10",
             ]
 
             [dependencies]
             normal-only = { version = "1.0", features = ["cat"] }
             normal-and-dev = { version = "1.0", features = ["cat"] }
+            optional-dep-feature = { version = "1.0", features = ["cat"], optional = true }
+            optional-namespaced = { version = "1.0", features = ["cat"], optional = true }
+            optional-renamed-namespaced10 = { version = "1.0", features = ["cat"], optional = true, package = "optional-renamed-namespaced" }
 
             [build-dependencies]
             build-only = { version = "1.0", features = ["cat"] }
@@ -1839,6 +1753,40 @@ You may press ctrl-c to skip waiting; the crate should be available shortly.
               "target": null,
               "version_req": "^1.0"
             },
+            {
+              "default_features": true,
+              "features": [
+                "cat"
+              ],
+              "kind": "normal",
+              "name": "optional-dep-feature",
+              "optional": true,
+              "target": null,
+              "version_req": "^1.0"
+            },
+            {
+              "default_features": true,
+              "features": [
+                "cat"
+              ],
+              "kind": "normal",
+              "name": "optional-namespaced",
+              "optional": true,
+              "target": null,
+              "version_req": "^1.0"
+            },
+            {
+              "default_features": true,
+              "explicit_name_in_toml": "optional-renamed-namespaced10",
+              "features": [
+                "cat"
+              ],
+              "kind": "normal",
+              "name": "optional-renamed-namespaced",
+              "optional": true,
+              "target": null,
+              "version_req": "^1.0"
+            },
             {
               "default_features": true,
               "features": [
@@ -1915,7 +1863,10 @@ You may press ctrl-c to skip waiting; the crate should be available shortly.
               "normal-and-dev/cat",
               "target-normal-only/cat",
               "target-build-only/cat",
-              "target-normal-and-dev/cat"
+              "target-normal-and-dev/cat",
+              "optional-dep-feature/cat",
+              "dep:optional-namespaced",
+              "dep:optional-renamed-namespaced10"
             ]
           },
           "homepage": "foo",
@@ -1966,6 +1917,22 @@ features = ["cat"]
 version = "1.0"
 features = ["cat"]
 
+[dependencies.optional-dep-feature]
+version = "1.0"
+features = ["cat"]
+optional = true
+
+[dependencies.optional-namespaced]
+version = "1.0"
+features = ["cat"]
+optional = true
+
+[dependencies.optional-renamed-namespaced10]
+version = "1.0"
+features = ["cat"]
+optional = true
+package = "optional-renamed-namespaced"
+
 [dev-dependencies.normal-and-dev]
 version = "1.0"
 features = ["cat"]
@@ -1982,6 +1949,9 @@ foo_feature = [
     "target-normal-only/cat",
     "target-build-only/cat",
     "target-normal-and-dev/cat",
+    "optional-dep-feature/cat",
+    "dep:optional-namespaced",
+    "dep:optional-renamed-namespaced10",
 ]
 
 [target."cfg(unix)".dependencies.target-normal-and-dev]
@@ -2005,6 +1975,7 @@ features = ["cat"]
         )],
     );
 }
+
 #[cargo_test]
 fn credentials_ambiguous_filename() {
     // `publish` generally requires a remote registry