Repository: rust-lang/cargo (rust)

Issue:
`CARGO_UNSTABLE_BUILD_STD` doesn't support multiple crates anymore
### Problem

```console
> CARGO_UNSTABLE_BUILD_STD=std,panic_abort cargo run
error: invalid character `,` in package name: `std,panic_abort`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)
```

This works via the CLI arg:

```console
> cargo run -Zbuild-std=std,panic_abort
...
   Compiling std v0.0.0 (/nix/store/gyinl9bb727ajna53pwz034xazca9aqz-rust-minimal-1.85.0-nightly-2025-01-02/lib/rustlib/src/rust/library/std)
...
   Compiling panic_abort v0.0.0 (/nix/store/gyinl9bb727ajna53pwz034xazca9aqz-rust-minimal-1.85.0-nightly-2025-01-02/lib/rustlib/src/rust/library/panic_abort)
...
Hello, world!
```

Tested on `cargo 1.85.0-nightly (d73d2caf9 2024-12-31)`. This used to work via the env-var too, I didn't fully bisect but at least in `cargo 1.84.0-nightly (e75214ea4 2024-10-25)` it worked.

### Steps

_No response_

### Possible Solution(s)

_No response_

### Notes

_No response_

### Version

```text
cargo 1.85.0-nightly (d73d2caf9 2024-12-31)
release: 1.85.0-nightly
commit-hash: d73d2caf9e41a39daf2a8d6ce60ec80bf354d2a7
commit-date: 2024-12-31
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: NixOS 24.11.0 [64-bit]
```

Pull request:
fix(build-std): parse as comma-separated list
### What does this PR try to resolve?

Restore to the behavior prior to 30d11ce1d9f06907d1e707c4fe379ebf57305a5e
Also extend `build-std-features` to support comma-separated list.

Fixes #15064

### How should we test and review this PR?

A test has been added.



Patch:
diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs
index 4e17898d6..774e725e0 100644
--- a/src/cargo/core/features.rs
+++ b/src/cargo/core/features.rs
@@ -759,7 +759,9 @@ unstable_cli_options!(
     avoid_dev_deps: bool = ("Avoid installing dev-dependencies if possible"),
     binary_dep_depinfo: bool = ("Track changes to dependency artifacts"),
     bindeps: bool = ("Allow Cargo packages to depend on bin, cdylib, and staticlib crates, and use the artifacts built by those crates"),
+    #[serde(deserialize_with = "deserialize_comma_separated_list")]
     build_std: Option<Vec<String>>  = ("Enable Cargo to compile the standard library itself as part of a crate graph compilation"),
+    #[serde(deserialize_with = "deserialize_comma_separated_list")]
     build_std_features: Option<Vec<String>>  = ("Configure features enabled for the standard library itself when building the standard library"),
     cargo_lints: bool = ("Enable the `[lints.cargo]` table"),
     checksum_freshness: bool = ("Use a checksum to determine if output is fresh rather than filesystem mtime"),
@@ -872,6 +874,24 @@ const STABILIZED_LINTS: &str = "The `[lints]` table is now always available.";
 const STABILIZED_CHECK_CFG: &str =
     "Compile-time checking of conditional (a.k.a. `-Zcheck-cfg`) is now always enabled.";
 
+fn deserialize_comma_separated_list<'de, D>(
+    deserializer: D,
+) -> Result<Option<Vec<String>>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let Some(list) = <Option<Vec<String>>>::deserialize(deserializer)? else {
+        return Ok(None);
+    };
+    let v = list
+        .iter()
+        .flat_map(|s| s.split(','))
+        .filter(|s| !s.is_empty())
+        .map(String::from)
+        .collect();
+    Ok(Some(v))
+}
+
 #[derive(Debug, Copy, Clone, Default, Deserialize, Ord, PartialOrd, Eq, PartialEq)]
 #[serde(default)]
 pub struct GitFeatures {


Test patch:
diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs
index ddaa168eb..6fe1172cd 100644
--- a/tests/testsuite/config.rs
+++ b/tests/testsuite/config.rs
@@ -2163,3 +2163,81 @@ gitoxide = \"fetch\"
         unstable_flags.gitoxide == expect
     }
 }
+
+#[cargo_test]
+fn build_std() {
+    let gctx = GlobalContextBuilder::new()
+        .env("CARGO_UNSTABLE_BUILD_STD", "core,std,panic_abort")
+        .build();
+    let value = gctx
+        .get::<Option<cargo::core::CliUnstable>>("unstable")
+        .unwrap()
+        .unwrap()
+        .build_std
+        .unwrap();
+    assert_eq!(
+        value,
+        vec![
+            "core".to_string(),
+            "std".to_string(),
+            "panic_abort".to_string(),
+        ],
+    );
+
+    let gctx = GlobalContextBuilder::new()
+        .config_arg("unstable.build-std=['core', 'std,panic_abort']")
+        .build();
+    let value = gctx
+        .get::<Option<cargo::core::CliUnstable>>("unstable")
+        .unwrap()
+        .unwrap()
+        .build_std
+        .unwrap();
+    assert_eq!(
+        value,
+        vec![
+            "core".to_string(),
+            "std".to_string(),
+            "panic_abort".to_string(),
+        ]
+    );
+
+    let gctx = GlobalContextBuilder::new()
+        .env(
+            "CARGO_UNSTABLE_BUILD_STD_FEATURES",
+            "backtrace,panic-unwind,windows_raw_dylib",
+        )
+        .build();
+    let value = gctx
+        .get::<Option<cargo::core::CliUnstable>>("unstable")
+        .unwrap()
+        .unwrap()
+        .build_std_features
+        .unwrap();
+    assert_eq!(
+        value,
+        vec![
+            "backtrace".to_string(),
+            "panic-unwind".to_string(),
+            "windows_raw_dylib".to_string(),
+        ]
+    );
+
+    let gctx = GlobalContextBuilder::new()
+        .config_arg("unstable.build-std-features=['backtrace', 'panic-unwind,windows_raw_dylib']")
+        .build();
+    let value = gctx
+        .get::<Option<cargo::core::CliUnstable>>("unstable")
+        .unwrap()
+        .unwrap()
+        .build_std_features
+        .unwrap();
+    assert_eq!(
+        value,
+        vec![
+            "backtrace".to_string(),
+            "panic-unwind".to_string(),
+            "windows_raw_dylib".to_string(),
+        ]
+    );
+}