From 8f434b081090f11f18c1de737cc77f37cf0d07fe Mon Sep 17 00:00:00 2001 From: divybot Date: Fri, 5 Jun 2026 14:38:33 +0530 Subject: [PATCH 1/2] feat(build): support optional system libnghttp2 linking Add opt-in linking against a system libnghttp2 (discovered via pkg-config) while keeping the bundled static build as the default and fallback. - Declare `links = "nghttp2"` so Cargo enforces a single native-library version and exposes DEP_NGHTTP2_ROOT / DEP_NGHTTP2_INCLUDE to downstream build scripts. - Add a `system` Cargo feature and a LIBNGHTTP2_SYS_USE_PKG_CONFIG env var to opt into system linking; both fall back to the bundled build when no suitable system library is found. - When using the system library, generate bindings against the system headers via pkg-config include paths. - Add a CI job that installs libnghttp2-dev and verifies the binary is dynamically linked against the system library. - Document the linking options in the README. Co-Authored-By: Divy Srivastava --- .github/workflows/rust.yml | 34 +++++++++ Cargo.toml | 13 ++++ README.md | 26 +++++++ build.rs | 137 ++++++++++++++++++++++++++++++++----- 4 files changed, 194 insertions(+), 16 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9da4f1d..76a9a31 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,3 +30,37 @@ jobs: run: cargo test --verbose --all-features - name: Check formatting run: cargo fmt -- --check --verbose + + # Verify the optional system-linking path: build against a system libnghttp2 + # discovered via pkg-config instead of the bundled sources. + system-link: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: 'recursive' + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.92.0 + - name: Install system libnghttp2 + run: | + sudo apt-get update + sudo apt-get install -y libnghttp2-dev pkg-config + pkg-config --modversion libnghttp2 + - name: Build against system libnghttp2 (feature) + run: cargo build --verbose --features system + - name: Test against system libnghttp2 (env var) + env: + LIBNGHTTP2_SYS_USE_PKG_CONFIG: "1" + run: cargo test --verbose + - name: Verify the dynamic library is actually linked + env: + LIBNGHTTP2_SYS_USE_PKG_CONFIG: "1" + run: | + bin=$(cargo test --no-run --message-format=json \ + | jq -r 'select(.profile.test == true and .executable != null) | .executable' \ + | head -n1) + echo "Test binary: $bin" + ldd "$bin" | grep -i nghttp2 + ldd "$bin" | grep -qi 'libnghttp2\.so' \ + && echo "OK: dynamically linked against system libnghttp2" \ + || (echo "ERROR: not linked against system libnghttp2" && exit 1) diff --git a/Cargo.toml b/Cargo.toml index f7e8ced..cd192e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,26 @@ homepage = "https://github.com/littledivy/libnghttp2" description = "FFI bindings to the HTTP/2 framing layer of nghttp2 C library" readme = "README.md" license = "Apache-2.0" +# Declare the native library this crate links, so Cargo can enforce a single +# version in the dependency graph and expose DEP_NGHTTP2_* metadata (root, +# include) to downstream build scripts. +links = "nghttp2" [lib] doctest = false +[features] +default = [] +# Link against a system libnghttp2 (discovered via pkg-config) instead of +# building the bundled copy. If no suitable system library is found, the build +# falls back to the bundled sources. Equivalent to setting the +# LIBNGHTTP2_SYS_USE_PKG_CONFIG environment variable at build time. +system = [] + [dependencies] libc = '0.2' [build-dependencies] cc = "1.0.24" bindgen = "0.71" +pkg-config = "0.3" diff --git a/README.md b/README.md index 626f2e0..94f7fbe 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,29 @@ nghttp2_submit_request(session_ptr, ptr::null(), headers.as_ptr(), headers.len() ``` See [examples/h2c_client.rs](examples/h2c_client.rs) for a simple HTTP/2 client implementation. + +## Linking + +By default this crate builds and statically links the bundled copy of nghttp2, +so it works out of the box with no system dependencies. + +Packagers (e.g. distros) can instead link against a system libnghttp2 discovered +via `pkg-config`. This is opt-in and falls back to the bundled build if no +suitable system library is found: + +- Enable the `system` Cargo feature: + + ```toml + [dependencies] + libnghttp2 = { version = "1", features = ["system"] } + ``` + +- Or set the `LIBNGHTTP2_SYS_USE_PKG_CONFIG=1` environment variable at build + time (no source changes required): + + ```sh + LIBNGHTTP2_SYS_USE_PKG_CONFIG=1 cargo build + ``` + +The crate declares `links = "nghttp2"`, so downstream `-sys`-style consumers can +read `DEP_NGHTTP2_ROOT` and `DEP_NGHTTP2_INCLUDE` from their build scripts. diff --git a/build.rs b/build.rs index 8810dd5..25bb695 100644 --- a/build.rs +++ b/build.rs @@ -64,6 +64,20 @@ fn main() { let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR not set")); let target = env::var("TARGET").expect("TARGET not set"); + // Rebuild when the system-linking opt-in changes. + println!("cargo:rerun-if-env-changed=LIBNGHTTP2_SYS_USE_PKG_CONFIG"); + + // Optionally link against a system libnghttp2 instead of the bundled copy. + // Falls back to the bundled build when no suitable system library is found. + if try_system_nghttp2(&out_dir) { + return; + } + + build_bundled(&out_dir, &target); +} + +// Build and link the bundled nghttp2 sources (the default / fallback path). +fn build_bundled(out_dir: &Path, target: &str) { let nghttp2_version = parse_nghttp2_version(); let install_dir = out_dir.join("i"); @@ -74,17 +88,77 @@ fn main() { generate_version_header(&include_dir, &nghttp2_version); copy_main_header(&include_dir); - build_nghttp2(&target, &include_dir, &lib_dir); + build_nghttp2(target, &include_dir, &lib_dir); generate_pkgconfig(&install_dir, &include_dir, &lib_dir, &nghttp2_version); - generate_bindings(&out_dir, &include_dir); + generate_bindings(out_dir, &include_dir); println!("cargo:root={}", install_dir.display()); + // Expose the bundled headers to downstream build scripts via + // DEP_NGHTTP2_INCLUDE (enabled by `links = "nghttp2"`). + println!("cargo:include={}", include_dir.display()); // Emit rerun-if-changed directives to avoid unnecessary rebuilds emit_rerun_if_changed(); } +// Returns true if the requested system libnghttp2 was found and linked. +// +// System linking is opt-in: it is attempted only when the `system` Cargo +// feature is enabled or the LIBNGHTTP2_SYS_USE_PKG_CONFIG environment variable +// is set to a truthy value. When opted in but no suitable library is found, we +// return false so the caller falls back to the bundled build. +fn try_system_nghttp2(out_dir: &Path) -> bool { + if !system_linking_requested() { + return false; + } + + let mut cfg = pkg_config::Config::new(); + // libnghttp2 in this crate tracks the 1.x series; require at least 1.0.0. + cfg.atleast_version("1.0.0"); + cfg.print_system_libs(false); + + match cfg.probe("libnghttp2") { + Ok(lib) => { + // pkg-config has already emitted the rustc-link-lib / link-search + // directives. Generate bindings against the system headers and expose + // the include paths to downstream crates. + generate_bindings_system(out_dir, &lib.include_paths); + + for path in &lib.include_paths { + if let Some(path) = path.to_str() { + println!("cargo:include={}", path); + } + } + println!( + "cargo:warning=libnghttp2: linking against system library (pkg-config)" + ); + true + } + Err(err) => { + println!( + "cargo:warning=libnghttp2: system library requested but not found \ + ({err}); falling back to the bundled build" + ); + false + } + } +} + +fn system_linking_requested() -> bool { + // `cargo:rustc-cfg`/features reach build scripts as CARGO_FEATURE_* vars. + if env::var_os("CARGO_FEATURE_SYSTEM").is_some() { + return true; + } + match env::var("LIBNGHTTP2_SYS_USE_PKG_CONFIG") { + Ok(value) => { + let value = value.trim(); + !value.is_empty() && value != "0" && !value.eq_ignore_ascii_case("false") + } + Err(_) => false, + } +} + fn emit_rerun_if_changed() { // Build script itself println!("cargo:rerun-if-changed=build.rs"); @@ -236,25 +310,56 @@ fn generate_pkgconfig( fn generate_bindings(out_dir: &Path, include_dir: &Path) { let header_path = include_dir.join("nghttp2/nghttp2.h"); + + let mut clang_args = vec![ + format!("-I{}", include_dir.display()), + "-Inghttp2/lib/includes".to_string(), + ]; + clang_args.extend(msvc_ssize_t_clang_arg()); + + write_bindings(out_dir, header_path.to_str().unwrap(), &clang_args); +} + +// Generate bindings against a system-provided libnghttp2 header. A small +// wrapper header is used so the system include directories resolve +// . +fn generate_bindings_system(out_dir: &Path, include_paths: &[PathBuf]) { + let wrapper = out_dir.join("nghttp2_wrapper.h"); + fs::write(&wrapper, "#include \n") + .expect("Failed to write bindgen wrapper header"); + + let mut clang_args: Vec = include_paths + .iter() + .map(|path| format!("-I{}", path.display())) + .collect(); + clang_args.extend(msvc_ssize_t_clang_arg()); + + write_bindings(out_dir, wrapper.to_str().unwrap(), &clang_args); +} + +// On Windows MSVC, clang/bindgen needs ssize_t defined. +fn msvc_ssize_t_clang_arg() -> Option { let target = env::var("TARGET").expect("TARGET not set"); + if !(target.contains("windows") && target.contains("msvc")) { + return None; + } - let mut builder = bindgen::Builder::default() - .header(header_path.to_str().unwrap()) - .clang_arg(format!("-I{}", include_dir.display())) - .clang_arg("-Inghttp2/lib/includes"); + let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH") + .expect("CARGO_CFG_TARGET_POINTER_WIDTH not set"); - // On Windows MSVC, define ssize_t for clang/bindgen - if target.contains("windows") && target.contains("msvc") { - let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH") - .expect("CARGO_CFG_TARGET_POINTER_WIDTH not set"); + let ssize_t_def = match pointer_width.as_str() { + "64" => "ssize_t=long long", + "32" => "ssize_t=long", + width => panic!("Unsupported pointer width: {}", width), + }; - let ssize_t_def = match pointer_width.as_str() { - "64" => "ssize_t=long long", - "32" => "ssize_t=long", - width => panic!("Unsupported pointer width: {}", width), - }; + Some(format!("-D{}", ssize_t_def)) +} - builder = builder.clang_arg(format!("-D{}", ssize_t_def)); +fn write_bindings(out_dir: &Path, header: &str, clang_args: &[String]) { + let mut builder = bindgen::Builder::default().header(header); + for arg in clang_args { + builder = builder.clang_arg(arg); } // Note: We don't use CargoCallbacks here because it would emit From 07e008aa655720dd7c9e798e761b3851fb002362 Mon Sep 17 00:00:00 2001 From: divybot Date: Fri, 5 Jun 2026 14:43:55 +0530 Subject: [PATCH 2/2] ci: select integration test binary for system-link check The lib's own unit-test harness has no tests and references no nghttp2 symbols, so --as-needed drops its DT_NEEDED entry and ldd shows nothing. Check the integration test binary, which actually calls nghttp2. Co-Authored-By: Divy Srivastava --- .github/workflows/rust.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 76a9a31..90b4313 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -56,8 +56,11 @@ jobs: env: LIBNGHTTP2_SYS_USE_PKG_CONFIG: "1" run: | + # Pick the integration test binary: it actually references nghttp2 + # symbols, so the linker keeps the DT_NEEDED entry (the lib's own empty + # unit-test harness would be dropped by --as-needed). bin=$(cargo test --no-run --message-format=json \ - | jq -r 'select(.profile.test == true and .executable != null) | .executable' \ + | jq -r 'select(.executable != null and .target.name == "integration") | .executable' \ | head -n1) echo "Test binary: $bin" ldd "$bin" | grep -i nghttp2