From 23bc53799188335f7205c8debb35efb8271c77c0 Mon Sep 17 00:00:00 2001 From: enthropy7 <221884178+enthropy7@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:03:05 +0300 Subject: [PATCH] forward whitelisted unstable cargo flags to host-side cargo commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `-Z bindeps` from `cargo-args` only reached `cargo rustdoc`, so `cargo metadata`, `generate-lockfile` and `fetch` still choked on manifests containing `artifact = "bin"` and the build died before rustdoc ran. rustwide 0.26 lets us cover its prepare phase too, which fails earliest of all — at `validate_manifest`. Host cargo runs unsandboxed, so this forwards a whitelist rather than the whole `cargo-args` — `bindeps` alone for now. `Build::fetch_build_std_dependencies` also needs the flags; that one is fixed inside rustwide, so this needs a rustwide release before bindeps crates build end to end. --- .../src/docbuilder/rustwide_builder.rs | 79 +++++++++++++++++-- .../tests/crates/bindeps-test/Cargo.toml | 14 ++++ .../bindeps-test/bindeps-helper/Cargo.toml | 8 ++ .../bindeps-test/bindeps-helper/src/main.rs | 1 + .../tests/crates/bindeps-test/src/lib.rs | 3 + .../templates/core/Cargo.toml.example | 1 + .../templates/core/about/metadata.html | 7 ++ crates/lib/metadata/lib.rs | 66 ++++++++++++++++ 8 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 crates/bin/docs_rs_builder/tests/crates/bindeps-test/Cargo.toml create mode 100644 crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/Cargo.toml create mode 100644 crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/src/main.rs create mode 100644 crates/bin/docs_rs_builder/tests/crates/bindeps-test/src/lib.rs diff --git a/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs b/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs index a486cfa9f3..08d6cd2bcf 100644 --- a/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs +++ b/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs @@ -106,9 +106,11 @@ fn load_metadata_from_rustwide( workspace: &Workspace, toolchain: &Toolchain, source_dir: &Path, + host_unstable_flags: &[String], ) -> Result { let res = Command::new(workspace, toolchain.cargo()) .args(["metadata", "--format-version", "1"]) + .args(host_unstable_flags) .current_directory(source_dir) .log_output(false) .run_capture()?; @@ -495,10 +497,17 @@ impl RustwideBuilder { } pub fn build_local_package(&mut self, path: &Path) -> Result { - let metadata = load_metadata_from_rustwide(&self.workspace, &self.toolchain, path) - .map_err(|err| { - err.context(format!("failed to load local package {}", path.display())) - })?; + let host_unstable_flags = Metadata::from_crate_root(path) + .map(|metadata| metadata.unstable_cargo_flags()) + .unwrap_or_default(); + + let metadata = load_metadata_from_rustwide( + &self.workspace, + &self.toolchain, + path, + &host_unstable_flags, + ) + .map_err(|err| err.context(format!("failed to load local package {}", path.display())))?; let package = metadata.root(); self.build_package( &package @@ -639,13 +648,19 @@ impl RustwideBuilder { let local_storage = tempfile::tempdir_in(&self.config.temp_dir)?; let mut algs = HashSet::new(); - let source_stats = { + let (source_stats, host_unstable_flags) = { let _span = info_span!("adding sources into database").entered(); debug!("adding sources into database"); let temp_dir = tempfile::tempdir_in(&self.config.temp_dir)?; krate.copy_source_to(&self.workspace, temp_dir.path())?; + // Read from this copy: rustwide's prepare phase needs the flags before the build + // closure below gets access to the sources. + let host_unstable_flags = Metadata::from_crate_root(temp_dir.path()) + .map(|metadata| metadata.unstable_cargo_flags()) + .unwrap_or_default(); + let stats = self.runtime.block_on( self.storage .store_all_in_archive(&source_archive_path(name, version), &temp_dir), @@ -654,11 +669,12 @@ impl RustwideBuilder { fs::remove_dir_all(temp_dir.path())?; algs.insert(stats.alg); - stats + (stats, host_unstable_flags) }; let successful = build_dir .build(&self.toolchain, &krate, self.prepare_sandbox(&limits)) + .extra_cargo_args(&host_unstable_flags) .run(|build| { // NOTE: rustwide will run `copy_source_to` again when preparing the call to this // closure. @@ -693,7 +709,8 @@ impl RustwideBuilder { let _span = info_span!("cargo_generate_lockfile").entered(); Command::new(&self.workspace, self.toolchain.cargo()) .current_directory(build.host_source_dir()) - .arg("generate-lockfile") + .args(["generate-lockfile"]) + .args(&host_unstable_flags) .run_capture()?; } { @@ -701,6 +718,7 @@ impl RustwideBuilder { Command::new(&self.workspace, self.toolchain.cargo()) .current_directory(build.host_source_dir()) .args(["fetch", "--locked"]) + .args(&host_unstable_flags) .run_capture()?; } res = @@ -1145,6 +1163,7 @@ impl RustwideBuilder { &self.workspace, &self.toolchain, &build.host_source_dir(), + &metadata.unstable_cargo_flags(), )?; let mut rustdoc_flags = vec![ @@ -2359,6 +2378,52 @@ mod tests { Ok(()) } + #[test] + #[ignore] + fn test_bindeps_metadata_with_unstable_flags() -> Result<()> { + let env = TestEnvironment::new()?; + let mut builder = env.build_builder()?; + builder.update_toolchain()?; + let crate_path = Path::new("tests/crates/bindeps-test"); + let metadata = Metadata::from_crate_root(crate_path)?; + let unstable_flags = metadata.unstable_cargo_flags(); + + assert!( + load_metadata_from_rustwide(&builder.workspace, &builder.toolchain, crate_path, &[]) + .is_err(), + "cargo metadata should fail without -Zbindeps", + ); + + assert!( + load_metadata_from_rustwide( + &builder.workspace, + &builder.toolchain, + crate_path, + &unstable_flags, + ) + .is_ok(), + "cargo metadata should succeed with -Zbindeps", + ); + + Ok(()) + } + + #[test] + #[ignore] + fn test_bindeps_crate_full_build() -> Result<()> { + let env = TestEnvironment::new()?; + let mut builder = env.build_builder()?; + builder.update_toolchain()?; + + assert!( + builder + .build_local_package(Path::new("tests/crates/bindeps-test"))? + .successful + ); + + Ok(()) + } + #[test] #[ignore] fn test_build_with_cpu_limit() -> Result<()> { diff --git a/crates/bin/docs_rs_builder/tests/crates/bindeps-test/Cargo.toml b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/Cargo.toml new file mode 100644 index 0000000000..1ba3805360 --- /dev/null +++ b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bindeps-test" +version = "0.1.0" +edition = "2021" + +[package.metadata.docs.rs] +cargo-args = ["-Zbindeps"] + +[build-dependencies] +bindeps-helper = { path = "bindeps-helper", artifact = "bin" } + +[lib] +name = "bindeps_test" +path = "src/lib.rs" diff --git a/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/Cargo.toml b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/Cargo.toml new file mode 100644 index 0000000000..fd016b705a --- /dev/null +++ b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "bindeps-helper" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "bindeps-helper" +path = "src/main.rs" diff --git a/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/src/main.rs b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/src/main.rs new file mode 100644 index 0000000000..f328e4d9d0 --- /dev/null +++ b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/bindeps-helper/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/bin/docs_rs_builder/tests/crates/bindeps-test/src/lib.rs b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/src/lib.rs new file mode 100644 index 0000000000..b4d75b5d4e --- /dev/null +++ b/crates/bin/docs_rs_builder/tests/crates/bindeps-test/src/lib.rs @@ -0,0 +1,3 @@ +pub fn hello() -> &'static str { + "Hello from bindeps-test!" +} diff --git a/crates/bin/docs_rs_web/templates/core/Cargo.toml.example b/crates/bin/docs_rs_web/templates/core/Cargo.toml.example index 9bc4706d9c..6cebbd96a3 100644 --- a/crates/bin/docs_rs_web/templates/core/Cargo.toml.example +++ b/crates/bin/docs_rs_web/templates/core/Cargo.toml.example @@ -48,4 +48,5 @@ rustdoc-args = ["--example-rustdoc-arg"] # List of command line arguments for `cargo`. # # These cannot be a subcommand, they may only be options. +# Of the unstable flags, only `bindeps` also reaches host-side cargo commands. cargo-args = ["-Z", "build-std"] diff --git a/crates/bin/docs_rs_web/templates/core/about/metadata.html b/crates/bin/docs_rs_web/templates/core/about/metadata.html index 9660ea9ff8..555ebc9589 100644 --- a/crates/bin/docs_rs_web/templates/core/about/metadata.html +++ b/crates/bin/docs_rs_web/templates/core/about/metadata.html @@ -17,6 +17,13 @@

Metadata for custom builds

{% filter highlight("toml") %} {%- include "core/Cargo.toml.example" -%} {% endfilter %} + +

+ For security reasons, docs.rs only forwards a whitelist of unstable cargo flags to + host-side cargo commands (cargo metadata, cargo fetch, + cargo generate-lockfile). Currently, only bindeps is + supported (as -Zbindeps or -Z bindeps). +

{%- endblock body %} diff --git a/crates/lib/metadata/lib.rs b/crates/lib/metadata/lib.rs index 797bd2f398..9f733f0167 100644 --- a/crates/lib/metadata/lib.rs +++ b/crates/lib/metadata/lib.rs @@ -146,6 +146,30 @@ pub struct Metadata { additional_targets: Vec, } +impl Metadata { + /// Unstable flags from `cargo_args` that may also be passed to cargo commands we run + /// outside the sandbox. These run with our privileges, so this is a whitelist. + pub fn unstable_cargo_flags(&self) -> Vec { + let mut flags = Vec::new(); + let mut iter = self.cargo_args.iter(); + while let Some(arg) = iter.next() { + if arg == "-Z" { + if let Some(value) = iter.next() + && value == "bindeps" + { + flags.push("-Z".to_string()); + flags.push(value.clone()); + } + } else if let Some(value) = arg.strip_prefix("-Z") + && value == "bindeps" + { + flags.push(arg.clone()); + } + } + flags + } +} + /// The targets that should be built for a crate. /// /// The `default_target` is the target to be used as the home page for that crate. @@ -553,6 +577,48 @@ mod test_parsing { let metadata = Metadata::from_str(manifest).unwrap(); assert!(!metadata.proc_macro); } + + #[test] + fn test_unstable_cargo_flags() { + let manifest = r#" + [package] + name = "test" + [package.metadata.docs.rs] + cargo-args = ["-Zbindeps", "--some-other-arg"] + "#; + let metadata = Metadata::from_str(manifest).unwrap(); + assert_eq!(metadata.unstable_cargo_flags(), vec!["-Zbindeps"]); + + let manifest = r#" + [package] + name = "test" + [package.metadata.docs.rs] + cargo-args = ["-Z", "bindeps", "--other"] + "#; + let metadata = Metadata::from_str(manifest).unwrap(); + assert_eq!(metadata.unstable_cargo_flags(), vec!["-Z", "bindeps"]); + + let manifest = r#" + [package] + name = "test" + [package.metadata.docs.rs] + cargo-args = ["-Zbindeps", "-Z", "build-std", "--offline"] + "#; + let metadata = Metadata::from_str(manifest).unwrap(); + assert_eq!(metadata.unstable_cargo_flags(), vec!["-Zbindeps"]); + + let manifest = r#" + [package] + name = "test" + [package.metadata.docs.rs] + cargo-args = ["--offline", "--locked"] + "#; + let metadata = Metadata::from_str(manifest).unwrap(); + assert!(metadata.unstable_cargo_flags().is_empty()); + + let metadata = Metadata::default(); + assert!(metadata.unstable_cargo_flags().is_empty()); + } } #[cfg(test)]