Bindgen's build failing in docsrs
23:29 23 Aug 2025

I have been trying to publish my crate's version on docs.rs but bindgen's build script fails in there but not locally, I am getting the following error

   Compiling nu-ansi-term v0.46.0
   Compiling addr2line v0.24.2
warning: pam-sys-fork@1.0.2-alpha5: Skipping PAM bindgen for [docs.rs](http://docs.rs)
error: failed to run custom build command for \`pam-sys-fork v1.0.2-alpha5\`

Caused by:
  process didn't exit successfully: \`/home/ramayen/Documents/projects/Spell/target/package/spell-framework-0.1.5/target/debug/build/pam-sys-fork-a860873abd45d4fc/build-script-build\` (exit status: 101)
  \--- stdout
  cargo:rustc-link-lib=pam
  cargo:rustc-link-lib=pam\_misc
  cargo:rerun-if-changed=wrapper.h
  cargo:warning=Skipping PAM bindgen for [docs.rs](http://docs.rs)
  \--- stderr
  thread 'main' panicked at /home/ramayen/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/clang-sys-1.8.1/src/lib.rs:1859:1:
  a \`libclang\` shared library is not loaded on this thread
  note: run with \`RUST\_BACKTRACE=1\` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...
error: failed to verify package tarball  

My crate is using a dependency(say A) which uses another dependency (say B). It is B which has the C-bindings. I have tried the following:-

  1. Primary approch was to feature gate dep A with #[cfg(docsrs)] and #[cfg(not(docsrs))] flags and custom feature flags by making dep A optional and adding the custom flags in default and disabling default for docs.rs like following:-

[package.metadata.docs.rs]
no-default-features = true
features = ["docs-only"]

[features]
default = ["i-slint-renderer-skia", "pam"]
docs-only = []
pam = ["pam-client2-fork"] # This is dep A.
  1. The above point didn't fix the issue. So, I forked both A and B dep to fix the issue in B's build.rs. I haven't worked with bindgen previously so my knowledge in ffi's are limited, yet I configured the build.rs with cfg docsrs flags in the build.rs along with putting the contents inside and if statement which only runs if docs.rs' env variable is set. Here is the modified build.rs.
fn main() {
   println!("cargo:rustc-link-lib=pam");
   if cfg!(target_os = "linux") {
       println!("cargo:rustc-link-lib=pam_misc");
   }
   println!("cargo:rerun-if-changed=wrapper.h");

   #[cfg(not(docsrs))]
   if std::env::var("DOCS_RS").is_err() {
       #[cfg(not(docsrs))]
       extern crate bindgen;

       use std::env;
       use std::path::PathBuf;
       println!("cargo:warning=Skipping PAM bindgen for docs.rs");
       // Prepare bindgen builder
       let mut builder = bindgen::Builder::default()
           .header("wrapper.h")
           .ctypes_prefix("libc")
           .opaque_type("pam_handle_t")
           .blocklist_type("va_list")
           .blocklist_type("__va_list")
           .blocklist_type("__builtin_va_list")
           .blocklist_type("__gnuc_va_list")
           .blocklist_type("__va_list_tag")
           .blocklist_function("pam_v.*")
           .blocklist_function("pam_syslog")
           .blocklist_function("pam_prompt")
           // Allow all PAM constants
           .allowlist_var("PAM_.*")
           // Allow all PAM functions..
           .allowlist_function("pam_.*")
           // ..except module related functions (pam_sm_*)
           .blocklist_function("pam_sm_.*");

       // Platform-specific adaptions
       if cfg!(target_os = "linux") {
           builder = builder
               .default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
               .raw_line("use libc::{uid_t, gid_t, group, passwd, spwd};")
               .blocklist_type(".*gid_t")
               .blocklist_type(".*uid_t")
               .blocklist_type("group")
               .blocklist_type("passwd")
               .blocklist_type("spwd");
       } else if cfg!(target_os = "freebsd") || cfg!(target_os = "netbsd") {
           // XXX: this should include all OS that use openPAM
           builder = builder
               // Use libc types so our signatures are slightly nicer
               .raw_line("use libc::passwd;")
               .blocklist_type("passwd");
       }

       #[cfg(not(docsrs))]
       let bindings = builder
           // Finish the builder and generate the bindings.
           .generate()
           // Unwrap the Result and panic on failure.
           .expect("Unable to generate bindings");

       // Write the bindings to the $OUT_DIR/bindings.rs file.
       let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

       #[cfg(not(docsrs))]
       bindings
           .write_to_file(out_path.join("bindings.rs"))
           .expect("Couldn't write bindings!");
   }
}

I have 2 questions:

  1. How can this issue is resolved?
  2. Since cargo publish recompiles everything every time. It becomes difficult to reiterate over the issue, any possible setup by which I can test these issues locally before pushing the docs.
rust deployment rust-cargo rust-bindgen