From 2e41210bfe00385193816044ce6edc2e9d065d19 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 14 Nov 2024 08:19:44 -0800 Subject: [PATCH 1/4] feat(sys): export detected nginx build configuration --- Cargo.lock | 18 ++--- nginx-sys/Cargo.toml | 5 ++ nginx-sys/build/main.rs | 146 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f18508b..b9f9fb0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,9 +148,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.15" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -534,8 +534,10 @@ name = "nginx-sys" version = "0.5.0" dependencies = [ "bindgen", + "cc", "duct", "flate2", + "regex", "tar", "ureq", "which", @@ -667,9 +669,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -679,9 +681,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -690,9 +692,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" diff --git a/nginx-sys/Cargo.toml b/nginx-sys/Cargo.toml index 66e2651c..97387ae0 100644 --- a/nginx-sys/Cargo.toml +++ b/nginx-sys/Cargo.toml @@ -5,6 +5,9 @@ categories = ["external-ffi-bindings"] description = "FFI bindings to NGINX" keywords = ["nginx", "ffi", "sys"] build = "build/main.rs" +# This field is required to export DEP_NGINX_ vars +# See https://github.com/rust-lang/cargo/issues/3544 +links = "nginx" edition.workspace = true license.workspace = true homepage.workspace = true @@ -15,8 +18,10 @@ rust-version.workspace = true [build-dependencies] bindgen = "0.70.1" +cc = "1.2.0" duct = { version = "0.13.7", optional = true } flate2 = { version = "1.0.28", optional = true } +regex = "1.11.1" tar = { version = "0.4.40", optional = true } ureq = { version = "2.9.6", features = ["tls"], optional = true } which = { version = "6.0.0", optional = true } diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index 97f4ef97..fadf50b2 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -2,14 +2,57 @@ extern crate bindgen; use std::env; use std::error::Error as StdError; -use std::fs::read_to_string; -use std::path::PathBuf; +use std::fs::{read_to_string, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; #[cfg(feature = "vendored")] mod vendored; const ENV_VARS_TRIGGERING_RECOMPILE: [&str; 2] = ["OUT_DIR", "NGX_OBJS"]; +/// The feature flags set by the nginx configuration script. +/// +/// This list is a subset of NGX_/NGX_HAVE_ macros known to affect the structure layout or module +/// avialiability. +/// +/// The flags will be exposed to the buildscripts of _direct_ dependendents of this crate as +/// `DEP_NGINX_FEATURES` environment variable. +/// The list of recognized values will be exported as `DEP_NGINX_FEATURES_CHECK`. +const NGX_CONF_FEATURES: &[&str] = &[ + "compat", + "debug", + "have_epollrdhup", + "have_file_aio", + "have_kqueue", + "http_cache", + "http_dav", + "http_gzip", + "http_realip", + "http_ssi", + "http_ssl", + "http_upstream_zone", + "http_v2", + "http_v3", + "http_x_forwarded_for", + "pcre", + "pcre2", + "quic", + "ssl", + "stream_ssl", + "stream_upstream_zone", + "threads", +]; + +/// The operating systems supported by the nginx configuration script +/// +/// The detected value will be exposed to the buildsrcipts of _direct_ dependents of this crate as +/// `DEP_NGINX_OS` environment variable. +/// The list of recognized values will be exported as `DEP_NGINX_OS_CHECK`. +const NGX_CONF_OS: &[&str] = &[ + "darwin", "freebsd", "gnu_hurd", "hpux", "linux", "solaris", "tru64", "win32", +]; + /// Function invoked when `cargo build` is executed. /// This function will download NGINX and all supporting dependencies, verify their integrity, /// extract them, execute autoconf `configure` for NGINX, compile NGINX and finally install @@ -37,11 +80,14 @@ fn main() -> Result<(), Box> { /// Generates Rust bindings for NGINX fn generate_binding(nginx_build_dir: PathBuf) { let autoconf_makefile_path = nginx_build_dir.join("Makefile"); - let clang_args: Vec = parse_includes_from_makefile(&autoconf_makefile_path) - .into_iter() + let includes = parse_includes_from_makefile(&autoconf_makefile_path); + let clang_args: Vec = includes + .iter() .map(|path| format!("-I{}", path.to_string_lossy())) .collect(); + print_cargo_metadata(&includes).expect("cargo dependency metadata"); + let bindings = bindgen::Builder::default() // Bindings will not compile on Linux without block listing this item // It is worth investigating why this is @@ -132,3 +178,95 @@ fn parse_includes_from_makefile(nginx_autoconf_makefile_path: &PathBuf) -> Vec

>(includes: &[T]) -> Result<(), Box> { + // Unquote and merge C string constants + let unquote_re = regex::Regex::new(r#""(.*?[^\\])"\s*"#).unwrap(); + let unquote = |data: &str| -> String { + unquote_re + .captures_iter(data) + .map(|c| c.get(1).unwrap().as_str()) + .collect::>() + .concat() + }; + + let mut ngx_features: Vec = vec![]; + let mut ngx_os = String::new(); + + let expanded = expand_definitions(includes)?; + for line in String::from_utf8(expanded)?.lines() { + let Some((name, value)) = line.trim().strip_prefix("RUST_CONF_").and_then(|x| x.split_once('=')) else { + continue; + }; + + let name = name.trim().to_ascii_lowercase(); + let value = value.trim(); + + if name == "nginx_build" { + println!("cargo::metadata=build={}", unquote(value)); + } else if name == "nginx_version" { + println!("cargo::metadata=version={}", unquote(value)); + } else if name == "nginx_version_number" { + println!("cargo::metadata=version_number={value}"); + } else if NGX_CONF_OS.contains(&name.as_str()) { + ngx_os = name; + } else if NGX_CONF_FEATURES.contains(&name.as_str()) && value != "0" { + ngx_features.push(name); + } + } + + println!( + "cargo::metadata=include={}", + // The str conversion is necessary because cargo directives must be valid UTF-8 + env::join_paths(includes.iter().map(|x| x.as_ref()))? + .to_str() + .expect("Unicode include paths") + ); + + // A quoted list of all recognized features to be passed to rustc-check-cfg. + println!("cargo::metadata=features_check=\"{}\"", NGX_CONF_FEATURES.join("\",\"")); + // A list of features enabled in the nginx build we're using + println!("cargo::metadata=features={}", ngx_features.join(",")); + + // A quoted list of all recognized operating systems to be passed to rustc-check-cfg. + println!("cargo::metadata=os_check=\"{}\"", NGX_CONF_OS.join("\",\"")); + // Current detected operating system + println!("cargo::metadata=os={ngx_os}"); + + Ok(()) +} + +fn expand_definitions>(includes: &[T]) -> Result, Box> { + let path = PathBuf::from(env::var("OUT_DIR")?).join("expand.c"); + let mut writer = std::io::BufWriter::new(File::create(&path)?); + + write!( + writer, + " +#include +#include + +RUST_CONF_NGINX_BUILD=NGINX_VER_BUILD +RUST_CONF_NGINX_VERSION=NGINX_VER +RUST_CONF_NGINX_VERSION_NUMBER=nginx_version +" + )?; + + for flag in NGX_CONF_FEATURES.iter().chain(NGX_CONF_OS.iter()) { + let flag = flag.to_ascii_uppercase(); + write!( + writer, + " +#if defined(NGX_{flag}) +RUST_CONF_{flag}=NGX_{flag} +#endif" + )?; + } + + writer.flush()?; + drop(writer); + + Ok(cc::Build::new().includes(includes).file(path).try_expand()?) +} From 8458a5eb1ff68999d693428b6417c66f2f10c9b5 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 14 Nov 2024 08:30:22 -0800 Subject: [PATCH 2/4] feat: expose DEP_NGINX_ vars to the high-level bindings The example build script converts `DEP_NGINX_FEATURES`, `DEP_NGINX_OS` and `DEP_NGINX_VERSION_NUMBER` received from `nginx-sys` to the `cfg` values for conditional compilation. See tests for usage examples. --- Cargo.lock | 1 + build.rs | 46 ++++++++++++++++++++++++++++++++ examples/Cargo.toml | 9 ++++--- tests/conditional-compilation.rs | 23 ++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 build.rs create mode 100644 tests/conditional-compilation.rs diff --git a/Cargo.lock b/Cargo.lock index b9f9fb0d..96260b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,7 @@ dependencies = [ "chrono", "http", "libc", + "nginx-sys", "ngx", "tokio", ] diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..400c7cc8 --- /dev/null +++ b/build.rs @@ -0,0 +1,46 @@ +/// Example buildscript for an nginx module. +/// +/// Due to the limitations of cargo[1], this buildscript _requires_ adding `nginx-sys` to the +/// direct dependencies of your crate. +/// +/// [1]: https://github.com/rust-lang/cargo/issues/3544 +fn main() { + // Generate `ngx_os` and `ngx_feature` cfg values + + // Specify acceptable values for `ngx_feature` + println!("cargo::rerun-if-env-changed=DEP_NGINX_FEATURES_CHECK"); + println!( + "cargo::rustc-check-cfg=cfg(ngx_feature, values({}))", + std::env::var("DEP_NGINX_FEATURES_CHECK").unwrap_or("any()".to_string()) + ); + // Read feature flags detected by nginx-sys and pass to the compiler. + println!("cargo::rerun-if-env-changed=DEP_NGINX_FEATURES"); + if let Ok(features) = std::env::var("DEP_NGINX_FEATURES") { + for feature in features.split(',').map(str::trim) { + println!("cargo::rustc-cfg=ngx_feature=\"{}\"", feature); + } + } + + // Specify acceptable values for `ngx_os` + println!("cargo::rerun-if-env-changed=DEP_NGINX_OS_CHECK"); + println!( + "cargo::rustc-check-cfg=cfg(ngx_os, values({}))", + std::env::var("DEP_NGINX_OS_CHECK").unwrap_or("any()".to_string()) + ); + // Read operating system detected by nginx-sys and pass to the compiler. + println!("cargo::rerun-if-env-changed=DEP_NGINX_OS"); + if let Ok(os) = std::env::var("DEP_NGINX_OS") { + println!("cargo::rustc-cfg=ngx_os=\"{}\"", os); + } + + // Generate cfg values for version checks + // println!("cargo::rustc-check-cfg=cfg(nginx1_27_0)"); + // println!("cargo::rerun-if-env-changed=DEP_NGINX_VERSION_NUMBER"); + // if let Ok(version) = std::env::var("DEP_NGINX_VERSION_NUMBER") { + // let version: u64 = version.parse().unwrap(); + // + // if version >= 1_027_000 { + // println!("cargo::rustc-cfg=nginx1_27_0"); + // } + // } +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 24be49a6..a75830bc 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,8 +8,13 @@ homepage.workspace = true repository.workspace = true rust-version.workspace = true -[dev-dependencies] +build = "../build.rs" + +[dependencies] +nginx-sys = { path = "../nginx-sys/", default-features = false } ngx = { path = "../", default-features = false } + +[dev-dependencies] aws-sign-v4 = "0.3.0" chrono = "0.4.23" http = "1.1.0" @@ -42,8 +47,6 @@ name = "async" path = "async.rs" crate-type = ["cdylib"] -[dependencies] - [features] default = ["export-modules", "ngx/vendored"] # Generate `ngx_modules` table with module exports diff --git a/tests/conditional-compilation.rs b/tests/conditional-compilation.rs new file mode 100644 index 00000000..9fb6de51 --- /dev/null +++ b/tests/conditional-compilation.rs @@ -0,0 +1,23 @@ +use ngx::ffi; + +#[test] +fn test_os_symbols() { + #[cfg(ngx_os = "freebsd")] + assert_eq!(ffi::NGX_FREEBSD, 1); + + #[cfg(ngx_os = "linux")] + assert_eq!(ffi::NGX_LINUX, 1); + + #[cfg(ngx_os = "darwin")] + assert_eq!(ffi::NGX_DARWIN, 1); +} + +#[test] +fn test_feature_symbols() { + let ev: ffi::ngx_event_t = unsafe { std::mem::zeroed() }; + + assert_eq!(ev.available, 0); + + #[cfg(ngx_feature = "have_kqueue")] + assert_eq!(ev.kq_errno, 0); +} From fd77e5166e6f0baea3fafe125104f3a4c183c0da Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 14 Nov 2024 09:18:22 -0800 Subject: [PATCH 3/4] fix: set compiler flags for MacOS in the build script The flags set in cargo config will be ignored if the `RUSTFLAGS` is set. --- .cargo/config.toml | 14 -------------- build.rs | 7 +++++++ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index f002391c..910c3da5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,16 +1,2 @@ -# -# https://stackoverflow.com/questions/28124221/error-linking-with-cc-failed-exit-code-1 -# -[target.aarch64-apple-darwin] -rustflags = [ - "-C", "link-arg=-undefined", - "-C", "link-arg=dynamic_lookup", -] -[target.x86_64-apple-darwin] -rustflags = [ - "-C", "link-arg=-undefined", - "-C", "link-arg=dynamic_lookup", -] - [env] CACHE_DIR = { value = ".cache", relative = true } diff --git a/build.rs b/build.rs index 400c7cc8..763ec1ba 100644 --- a/build.rs +++ b/build.rs @@ -43,4 +43,11 @@ fn main() { // println!("cargo::rustc-cfg=nginx1_27_0"); // } // } + + // Generate required compiler flags + if cfg!(target_os = "macos") { + // https://stackoverflow.com/questions/28124221/error-linking-with-cc-failed-exit-code-1 + println!("cargo::rustc-link-arg=-undefined"); + println!("cargo::rustc-link-arg=dynamic_lookup"); + } } From b8b638366eeb13612f385fd509614d6fbb6c3f31 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Thu, 14 Nov 2024 14:53:06 -0800 Subject: [PATCH 4/4] docs(sys): move crate documentation to README Document cargo metadata variables set by the build script. --- nginx-sys/README.md | 129 +++++++++++++++++++++++++++++++++++++++++++ nginx-sys/src/lib.rs | 30 +--------- 2 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 nginx-sys/README.md diff --git a/nginx-sys/README.md b/nginx-sys/README.md new file mode 100644 index 00000000..35fe7fb6 --- /dev/null +++ b/nginx-sys/README.md @@ -0,0 +1,129 @@ +# nginx-sys + +The `nginx-sys` crate provides low-level bindings for the nginx C API, allowing +Rust applications to interact with nginx servers and modules. + +## Usage + +Add `nginx-sys` as a dependency in your `Cargo.toml`: + +```toml +[dependencies] +nginx-sys = "0.5.0" +``` + +## Features + +- `vendored`: Enables the build scripts to download and build a copy of nginx + source and link against it. + This feature is enabled by default. + +## Output variables + +Following metadata variables are passed to the build scripts of any **direct** +dependents of the package. + +Check the [using another sys crate] and the [links manifest key] sections of the +Cargo Book for more details about passing metadata between packages. + +### `DEP_NGINX_FEATURES` + +nginx has various configuration options that may affect the availability of +functions, constants and structure fields. This is not something that can be +detected at runtime, as an attempt to use a symbol unavailable in the bindings +would result in compilation error. + +`DEP_NGINX_FEATURES_CHECK` contains the full list of feature flags supported +by `nginx-sys`, i.e. everything that can be used for feature checks. +The most common use for this variable is to specify [`cargo::rustc-check-cfg`]. + +`DEP_NGINX_FEATURES` contains the list of features enabled in the version of +nginx being built against. + +An example of a build script with these variables: +```rust +// Specify acceptable values for `ngx_feature`. +println!("cargo::rerun-if-env-changed=DEP_NGINX_FEATURES_CHECK"); +println!( + "cargo::rustc-check-cfg=cfg(ngx_feature, values({}))", + std::env::var("DEP_NGINX_FEATURES_CHECK").unwrap_or("any()".to_string()) +); +// Read feature flags detected by nginx-sys and pass to the compiler. +println!("cargo::rerun-if-env-changed=DEP_NGINX_FEATURES"); +if let Ok(features) = std::env::var("DEP_NGINX_FEATURES") { + for feature in features.split(',').map(str::trim) { + println!("cargo::rustc-cfg=ngx_feature=\"{}\"", feature); + } +} +``` + +And an usage example: +```rust +#[cfg(ngx_feature = "debug")] +println!("this nginx binary was built with debug logging enabled"); +``` + +### `DEP_NGINX_OS` + +Version, as detected by the nginx configuration script. + +`DEP_NGINX_OS_CHECK` contains the full list of supported values, and +`DEP_NGINX_OS` the currently detected one. + +Usage examples: +```rust +// Specify acceptable values for `ngx_os` +println!("cargo::rerun-if-env-changed=DEP_NGINX_OS_CHECK"); +println!( + "cargo::rustc-check-cfg=cfg(ngx_os, values({}))", + std::env::var("DEP_NGINX_OS_CHECK").unwrap_or("any()".to_string()) +); +// Read operating system detected by nginx-sys and pass to the compiler. +println!("cargo::rerun-if-env-changed=DEP_NGINX_OS"); +if let Ok(os) = std::env::var("DEP_NGINX_OS") { + println!("cargo::rustc-cfg=ngx_os=\"{}\"", os); +} +``` + +```rust +#[cfg(ngx_os = "freebsd")] +println!("this nginx binary was built on FreeBSD"); +``` + +### Version and build information + +- `DEP_NGINX_VERSION_NUMBER`: + a numeric representation with 3 digits for each component: `1026002` +- `DEP_NGINX_VERSION`: + human-readable string in a product/version format: `nginx/1.26.2` +- `DEP_NGINX_BUILD`: + version string with the optional build name (`--build=`) included: + `nginx/1.25.5 (nginx-plus-r32)` + +Usage example: +```rust +println!("cargo::rustc-check-cfg=cfg(nginx1_27_0)"); +println!("cargo::rerun-if-env-changed=DEP_NGINX_VERSION_NUMBER"); +if let Ok(version) = std::env::var("DEP_NGINX_VERSION_NUMBER") { + let version: u64 = version.parse().unwrap(); + + if version >= 1_027_000 { + println!("cargo::rustc-cfg=nginx1_27_0"); + } +} +``` + +## Examples + +### Get nginx Version + +This example demonstrates how to retrieve the version of the nginx server. + +```rust,no_run +use nginx_sys::nginx_version; + +println!("nginx version: {}", nginx_version); +``` +[using another sys crate]: https://doc.rust-lang.org/nightly/cargo/reference/build-script-examples.html#using-another-sys-crate +[links manifest key]: https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html#the-links-manifest-key +[`cargo::rustc-check-cfg`]: https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html#rustc-check-cfg diff --git a/nginx-sys/src/lib.rs b/nginx-sys/src/lib.rs index db9a05e4..8264428f 100644 --- a/nginx-sys/src/lib.rs +++ b/nginx-sys/src/lib.rs @@ -1,32 +1,4 @@ -//! # nginx-sys -//! -//! The `nginx-sys` crate provides low-level bindings for the nginx C API, allowing Rust applications to interact with nginx servers and modules. -//! -//! ## Usage -//! -//! Add `nginx-sys` as a dependency in your `Cargo.toml`: -//! -//! ```toml -//! [dependencies] -//! nginx-sys = "0.1.0" -//! ``` -//! -//! ## Features -//! -//! - `build`: Enables the build scripts to compile and link against the nginx C library. This feature is enabled by default. -//! -//! ## Examples -//! -//! ### Get Nginx Version -//! -//! This example demonstrates how to retrieve the version of the nginx server. -//! -//! ```rust,no_run -//! use nginx_sys::nginx_version; -//! -//! println!("Nginx version: {}", nginx_version); -//! ``` -//! +#![doc = include_str!("../README.md")] #![warn(missing_docs)] use std::fmt;