Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 100 additions & 9 deletions crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI failed cause new artifact-based test hit rustwide local manifest validation before our build path.
i changed the test to check cargo metadata flag forwarding directly - fails without -Zbindeps, passes with it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thing I would like to keep a full build test to be sure the whole feature keeps working.

CI failed cause new artifact-based test hit rustwide local manifest validation before our build path.

Which part fails?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fails in rustwide’s local crate manifest validation step (before the docs.rs build path): rustwide::prepare::validate_manifest runs cargo metadata --manifest-path Cargo.toml --no-deps without -Zbindeps, so Cargo rejects artifact = "...".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully follow.

I can only see that build_local_package calls load_metadata_from_rustwide, which you both adapt in this PR?

So from what I see, any local crate manifest validation path would also be called in the "normal" build path?

It's totally possible I'm just missing context or details, but since I'll need to maintain it, I want to fully understand :)

Can you push a commit where I can see the failing test how you tried it? That would help .

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally I'm super happy this can make progress

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried to build the crate from the issue using your branch, using the "normal" docs.rs build, not test.

2026-03-14T12:35:01.734600Z  INFO build_package{name=KrateName("protoc-plugin-by-closure") version=Version(Version { major: 0, minor: 1, patch: 6 }) kind=CratesIo collect_metrics=true}:build_package_inner{name=KrateName("prot
oc-plugin-by-closure") version=Version(Version { major: 0, minor: 1, patch: 6 }) kind=CratesIo crate_id=CrateId(1) release_id=ReleaseId(1) build_id=BuildId(1) collect_metrics=true}: rustwide::cmd: running `Command { std: CARG
O_HOME="/opt/docsrs/rustwide/cargo-home" RUSTUP_HOME="/opt/docsrs/rustwide/rustup-home" "/opt/docsrs/rustwide/cargo-home/bin/cargo" "+nightly" "metadata" "--manifest-path" "Cargo.toml" "--no-deps", kill_on_drop: false }`
2026-03-14T12:35:02.031737Z DEBUG build_package{name=KrateName("protoc-plugin-by-closure") version=Version(Version { major: 0, minor: 1, patch: 6 }) kind=CratesIo collect_metrics=true}:update_build_with_error{build_id=BuildId
(1) build_error=Some(Other(invalid Cargo.toml syntax

Stack backtrace:
   0: anyhow::error::<impl core::convert::From<E> for anyhow::Error>::from
   1: <T as core::convert::Into<U>>::into
   2: rustwide::prepare::Prepare::validate_manifest
   3: rustwide::prepare::Prepare::prepare
   4: rustwide::build::BuildDirectory::run
             at ./usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustwide-0.22.1/src/build.rs:197:17

This looks like the failing test that you saw was a sign that this feature wouldn't have worked at all for builds.

Did you manually run a build to test it?

In any case: I believe you should re-add the test, and then we can figure out what is necessary to fix it.

Perhaps even a change to rustwide?

Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,17 @@ fn load_metadata_from_rustwide(
workspace: &Workspace,
toolchain: &Toolchain,
source_dir: &Path,
host_unstable_flags: &[String],
) -> Result<CargoMetadata> {
let mut args = vec!["metadata", "--format-version", "1"];
// Add whitelisted unstable flags (currently `bindeps`) for host-side `cargo metadata`.
// See https://github.com/rust-lang/docs.rs/issues/2710
let host_unstable_flag_refs: Vec<&str> =
host_unstable_flags.iter().map(|s| s.as_str()).collect();
args.extend(host_unstable_flag_refs);

let res = Command::new(workspace, toolchain.cargo())
.args(&["metadata", "--format-version", "1"])
.args(&args)
.cd(source_dir)
.log_output(false)
.run_capture()?;
Expand Down Expand Up @@ -487,10 +495,17 @@ impl RustwideBuilder {
}

pub fn build_local_package(&mut self, path: &Path) -> Result<BuildPackageSummary> {
let metadata = load_metadata_from_rustwide(&self.workspace, &self.toolchain, path)
.map_err(|err| {
err.context(format!("failed to load local package {}", path.display()))
})?;
// Read docs.rs metadata first to get host-side unstable cargo flags (e.g., `-Z bindeps`).
let docsrs_metadata = Metadata::from_crate_root(path).unwrap_or_default();
let host_unstable_flags = docsrs_metadata.unstable_cargo_flags();

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
Expand Down Expand Up @@ -686,18 +701,34 @@ impl RustwideBuilder {
if !res.successful() && cargo_lock.exists() {
info!("removing lockfile and reattempting build");
std::fs::remove_file(cargo_lock)?;

// Get host-side unstable cargo flags for host-side cargo commands.
let host_unstable_flags = metadata.unstable_cargo_flags();

{
let _span = info_span!("cargo_generate_lockfile").entered();
let mut args = vec!["generate-lockfile"];
let flags_refs: Vec<&str> = host_unstable_flags
.iter()
.map(|s| s.as_str())
.collect();
args.extend(flags_refs.iter());
Command::new(&self.workspace, self.toolchain.cargo())
.cd(build.host_source_dir())
.args(&["generate-lockfile"])
.args(&args)
.run_capture()?;
}
{
let _span = info_span!("cargo fetch --locked").entered();
let mut args = vec!["fetch", "--locked"];
let flags_refs: Vec<&str> = host_unstable_flags
.iter()
.map(|s| s.as_str())
.collect();
args.extend(flags_refs.iter());
Command::new(&self.workspace, self.toolchain.cargo())
.cd(build.host_source_dir())
.args(&["fetch", "--locked"])
.args(&args)
.run_capture()?;
}
res =
Expand Down Expand Up @@ -1114,6 +1145,7 @@ impl RustwideBuilder {
&self.workspace,
&self.toolchain,
&build.host_source_dir(),
&metadata.unstable_cargo_flags(),
)?;

let mut rustdoc_flags = vec![
Expand Down Expand Up @@ -1824,7 +1856,6 @@ mod tests {
#[ignore]
fn test_proc_macro(crate_: &'static str, version: Version) -> Result<()> {
let env = TestEnvironment::new()?;

let crate_ = KrateName::from_static(crate_);

let mut builder = env.build_builder()?;
Expand Down Expand Up @@ -2079,7 +2110,6 @@ mod tests {
#[ignore]
fn test_no_implicit_features_for_optional_dependencies_with_dep_syntax() -> Result<()> {
let env = TestEnvironment::new()?;

let krate = KrateName::from_static("optional-dep");

let mut builder = env.build_builder()?;
Expand Down Expand Up @@ -2263,6 +2293,67 @@ mod tests {
Ok(())
}

#[test]
#[ignore] // Requires full build environment
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();

// The test crate contains a real `artifact = "bin"` dependency, so metadata
// must fail without `-Zbindeps`.
assert!(
load_metadata_from_rustwide(&builder.workspace, &builder.toolchain, crate_path, &[])
.is_err(),
"cargo metadata should fail without -Zbindeps",
);

// With the fix, host-side cargo metadata receives the whitelisted unstable flag.
assert!(
load_metadata_from_rustwide(
&builder.workspace,
&builder.toolchain,
crate_path,
&unstable_flags,
)
.is_ok(),
"cargo metadata should succeed with -Zbindeps",
);

Ok(())
}

#[test]
#[ignore] // Requires full build environment
fn test_bindeps_crate_full_build() -> Result<()> {
// Full build path for a crate using artifact dependencies (`-Zbindeps`).
//
// This currently fails because rustwide's `Prepare::prepare()` runs
// `cargo metadata`, `cargo generate-lockfile`, and `cargo fetch`
// without the `-Zbindeps` flag, causing `InvalidCargoTomlSyntax` at
// the `validate_manifest` step — before our build closure even runs.
//
// Fixing this requires rustwide to accept extra cargo args for its
// prepare phase (see https://github.com/rust-lang/rustwide/pull/119).
//
// TODO: flip this assertion to `assert!(... .successful)` once rustwide
// is updated with extra_cargo_args support.
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<()> {
Expand Down
15 changes: 15 additions & 0 deletions crates/bin/docs_rs_builder/tests/crates/bindeps-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "bindeps-test"
version = "0.1.0"
edition = "2021"

[package.metadata.docs.rs]
cargo-args = ["-Zbindeps"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More a question, because I don't know:

Would this bindeps-test crate fail the docs-build without the changes in this PR? Or would the arg just be added to the main build, and the other calls (cargo metadata) would fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi again! thanks for your kind words :)

i rebased the branch and removed artifacts from my builds and tests, so the PR now only contains the intended files.

about your question on bindeps-test: without this PR, docs.rs would fail before the main rustdoc build, during host-side cargo commands (cargo metadata / lockfile/fetch path), because the manifest uses artifact dependencies and Cargo requires -Z bindeps for parsing/resolution there. normal docs build command already received cargo-args. the missing part was forwarding the required flag to those host-side commands too.

i also fixed the new audit failure by updating quinn-proto in Cargo.lock

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the last piece missing for me :) Thank you for working on this!

One thing I'm still not sure about (might be lack of knowledge) is if this test crate would really fail to build without this PR?

Wouldn't a cleaner example be one where we actually artifact dependencies? like the one mentioned in #2710?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, thanks for the suggestion. changed bindeps-test to use a real artifact dependency, now it fails on cargo metadata without -Z bindeps and succeeds with it. and it demonstrates exactly why forwarding -Zbindeps to host-side cargo commands is needed.


[build-dependencies]
# This requires `-Z bindeps` to be accepted by cargo metadata/fetch.
bindeps-helper = { path = "bindeps-helper", artifact = "bin" }

[lib]
name = "bindeps_test"
path = "src/lib.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "bindeps-helper"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "bindeps-helper"
path = "src/main.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Test crate for bindeps support
//!
//! This crate uses unstable cargo feature `bindeps` (artifact dependencies).
//! It should build on docs.rs when the fix for #2710 is applied.

pub fn hello() -> &'static str {
"Hello from bindeps-test!"
}

3 changes: 3 additions & 0 deletions crates/bin/docs_rs_web/templates/core/Cargo.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ rustdoc-args = ["--example-rustdoc-arg"]
# List of command line arguments for `cargo`.
#
# These cannot be a subcommand, they may only be options.
# For security reasons, docs.rs only forwards a whitelist of unstable flags to
# host-side cargo commands (`metadata`, `fetch`, `generate-lockfile`).
# Currently that whitelist contains only `bindeps` (`-Zbindeps` or `-Z bindeps`).
cargo-args = ["-Z", "build-std"]
7 changes: 7 additions & 0 deletions crates/bin/docs_rs_web/templates/core/about/metadata.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ <h1>Metadata for custom builds</h1>
{% filter highlight("toml") %}
{%- include "core/Cargo.toml.example" -%}
{% endfilter %}

<p>
For security reasons, docs.rs only forwards a whitelist of unstable cargo flags to
host-side cargo commands (<code>cargo metadata</code>, <code>cargo fetch</code>,
<code>cargo generate-lockfile</code>). Currently, only <code>bindeps</code> is
supported (as <code>-Zbindeps</code> or <code>-Z bindeps</code>).
</p>
</div>
</div>
{%- endblock body %}
114 changes: 114 additions & 0 deletions crates/lib/metadata/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ pub struct Metadata {
additional_targets: Vec<String>,
}

impl Metadata {
/// Return unstable cargo flags from `cargo_args` that are allowed on host commands.
///
/// Most host-side cargo invocations on docs.rs are not sandboxed, so only whitelisted
/// unstable flags are allowed here.
///
/// Currently this includes only `bindeps`, accepted as either `-Zbindeps` or
/// `-Z bindeps`.
pub fn unstable_cargo_flags(&self) -> Vec<String> {
let mut flags = Vec::new();
let mut iter = self.cargo_args.iter();
while let Some(arg) = iter.next() {
if arg == "-Z" {
// `-Z bindeps` style (two separate args)
if let Some(value) = iter.next() {
if value == "bindeps" {
flags.push("-Z".to_string());
flags.push(value.clone());
}
}
} else if let Some(value) = arg.strip_prefix("-Z") {
if value == "bindeps" {
// `-Zbindeps` style (single arg)
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.
Expand Down Expand Up @@ -530,6 +561,53 @@ mod test_parsing {
let metadata = Metadata::from_str(manifest).unwrap();
assert!(!metadata.proc_macro);
}

#[test]
fn test_unstable_cargo_flags() {
// Test `-Zbindeps` style (single arg)
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"]);

// Test `-Z bindeps` style (two separate args)
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"]);

// Test that only whitelisted flags are returned.
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"]);

// Test no unstable flags
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());

// Test empty cargo-args
let metadata = Metadata::default();
assert!(metadata.unstable_cargo_flags().is_empty());
}
}

#[cfg(test)]
Expand Down Expand Up @@ -852,4 +930,40 @@ mod test_calculations {
];
assert_eq!(metadata.cargo_args(&[], &[]), expected_args);
}

#[test]
fn test_bindeps_cargo_args_parsing() {
use std::str::FromStr;
// Test that cargo-args with -Zbindeps is correctly parsed.
// This test demonstrates the issue: these flags are parsed but not
// passed to cargo metadata/fetch commands (see #2710).
let manifest = r#"
[package]
name = "test"
[package.metadata.docs.rs]
cargo-args = ["-Zbindeps"]
"#;
let metadata = Metadata::from_str(manifest).unwrap();
assert_eq!(metadata.cargo_args, vec!["-Zbindeps"]);

// After fix: unstable_cargo_flags() correctly extracts and returns the flags
assert_eq!(metadata.unstable_cargo_flags(), vec!["-Zbindeps"]);
}

#[test]
fn test_bindeps_separate_args_parsing() {
use std::str::FromStr;
// Test -Z bindeps style (two separate args)
let manifest = r#"
[package]
name = "test"
[package.metadata.docs.rs]
cargo-args = ["-Z", "bindeps"]
"#;
let metadata = Metadata::from_str(manifest).unwrap();
assert_eq!(metadata.cargo_args, vec!["-Z", "bindeps"]);

// After fix: unstable_cargo_flags() correctly extracts and returns the flags
assert_eq!(metadata.unstable_cargo_flags(), vec!["-Z", "bindeps"]);
}
}
Loading