nginx_src/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::ffi::OsString;
5use std::path::{Path, PathBuf};
6use std::process::Output;
7use std::{env, io, thread};
8
9mod download;
10mod verifier;
11
12static NGINX_DEFAULT_SOURCE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/nginx");
13
14const NGINX_BUILD_INFO: &str = "last-build-info";
15const NGINX_BINARY: &str = "nginx";
16
17static NGINX_CONFIGURE_BASE: &[&str] = &[
18    "--with-compat",
19    "--with-http_realip_module",
20    "--with-http_ssl_module",
21    "--with-http_v2_module",
22    "--with-stream",
23    "--with-stream_realip_module",
24    "--with-stream_ssl_module",
25    "--with-threads",
26];
27
28const ENV_VARS_TRIGGERING_RECOMPILE: [&str; 10] = [
29    "CACHE_DIR",
30    "CARGO_MANIFEST_DIR",
31    "CARGO_TARGET_TMPDIR",
32    "NGX_CONFIGURE_ARGS",
33    "NGX_CFLAGS",
34    "NGX_LDFLAGS",
35    "NGX_VERSION",
36    "OPENSSL_VERSION",
37    "PCRE2_VERSION",
38    "ZLIB_VERSION",
39];
40
41/*
42###########################################################################
43# NGINX Build Functions - Everything below here is for building NGINX     #
44###########################################################################
45
46In order to build Rust bindings for NGINX using the bindgen crate, we need
47to do the following:
48
49 1. Obtain a copy of the NGINX source code and the necessary dependencies:
50    OpenSSL, PCRE2, Zlib.
51 3. Run autoconf `configure` for NGINX.
52 4. Compile NGINX.
53 5. Read the autoconf generated makefile for NGINX and configure bindgen
54    to generate Rust bindings based on the includes in the makefile.
55*/
56
57/// Outputs cargo instructions required for using this crate from a buildscript.
58pub fn print_cargo_metadata() {
59    for file in ["lib.rs", "download.rs", "verifier.rs"] {
60        println!(
61            "cargo::rerun-if-changed={path}/src/{file}",
62            path = env!("CARGO_MANIFEST_DIR")
63        )
64    }
65
66    for var in ENV_VARS_TRIGGERING_RECOMPILE {
67        println!("cargo::rerun-if-env-changed={var}");
68    }
69}
70
71/// Builds a copy of NGINX sources, either bundled with the crate or downloaded from the network.
72pub fn build(build_dir: impl AsRef<Path>) -> io::Result<(PathBuf, PathBuf)> {
73    let source_dir = PathBuf::from(NGINX_DEFAULT_SOURCE_DIR);
74    let build_dir = build_dir.as_ref().to_owned();
75
76    let (source_dir, vendored_flags) = download::prepare(&source_dir, &build_dir)?;
77
78    let flags = nginx_configure_flags(&vendored_flags);
79
80    configure(&source_dir, &build_dir, &flags)?;
81
82    make(&source_dir, &build_dir, ["build"])?;
83
84    Ok((source_dir, build_dir))
85}
86
87/// Returns the options NGINX was built with
88fn build_info(source_dir: &Path, configure_flags: &[String]) -> String {
89    // Flags should contain strings pointing to OS/platform as well as dependency versions,
90    // so if any of that changes, it can trigger a rebuild
91    format!("{:?}|{}", source_dir, configure_flags.join(" "))
92}
93
94/// Generate the flags to use with autoconf `configure` for NGINX.
95fn nginx_configure_flags(vendored: &[String]) -> Vec<String> {
96    let mut nginx_opts: Vec<String> = NGINX_CONFIGURE_BASE
97        .iter()
98        .map(|x| String::from(*x))
99        .collect();
100
101    nginx_opts.extend(vendored.iter().map(Into::into));
102
103    if let Ok(extra_args) = env::var("NGX_CONFIGURE_ARGS") {
104        // FIXME: shell style split?
105        nginx_opts.extend(extra_args.split_whitespace().map(Into::into));
106    }
107
108    if let Ok(cflags) = env::var("NGX_CFLAGS") {
109        nginx_opts.push(format!("--with-cc-opt={cflags}"));
110    }
111
112    if let Ok(ldflags) = env::var("NGX_LDFLAGS") {
113        nginx_opts.push(format!("--with-ld-opt={ldflags}"));
114    }
115
116    nginx_opts
117}
118
119/// Runs external process invoking autoconf `configure` for NGINX.
120fn configure(source_dir: &Path, build_dir: &Path, flags: &[String]) -> io::Result<()> {
121    let build_info = build_info(source_dir, flags);
122
123    if build_dir.join("Makefile").is_file()
124        && build_dir.join(NGINX_BINARY).is_file()
125        && matches!(
126            std::fs::read_to_string(build_dir.join(NGINX_BUILD_INFO)).map(|x| x == build_info),
127            Ok(true)
128        )
129    {
130        println!("Build info unchanged, skipping configure");
131        return Ok(());
132    }
133
134    println!("Using NGINX source at {source_dir:?}");
135
136    let configure = ["configure", "auto/configure"]
137        .into_iter()
138        .map(|x| source_dir.join(x))
139        .find(|x| x.is_file())
140        .ok_or(io::ErrorKind::NotFound)?;
141
142    println!(
143        "Running NGINX configure script with flags: {:?}",
144        flags.join(" ")
145    );
146
147    let mut build_dir_arg: OsString = "--builddir=".into();
148    build_dir_arg.push(build_dir);
149
150    let mut flags: Vec<OsString> = flags.iter().map(|x| x.into()).collect();
151    flags.push(build_dir_arg);
152
153    let output = duct::cmd(configure, flags)
154        .dir(source_dir)
155        .stderr_to_stdout()
156        .run()?;
157
158    if !output.status.success() {
159        println!("configure failed with {:?}", output.status);
160        return Err(io::ErrorKind::Other.into());
161    }
162
163    let _ = std::fs::write(build_dir.join(NGINX_BUILD_INFO), build_info);
164
165    Ok(())
166}
167
168/// Runs `make` within the NGINX source directory as an external process.
169fn make<U>(source_dir: &Path, build_dir: &Path, extra_args: U) -> io::Result<Output>
170where
171    U: IntoIterator,
172    U::Item: Into<OsString>,
173{
174    // Level of concurrency to use when building nginx - cargo nicely provides this information
175    let num_jobs = match env::var("NUM_JOBS") {
176        Ok(s) => s.parse::<usize>().ok(),
177        Err(_) => thread::available_parallelism().ok().map(|n| n.get()),
178    }
179    .unwrap_or(1);
180
181    let mut args = vec![
182        OsString::from("-f"),
183        build_dir.join("Makefile").into(),
184        OsString::from("-j"),
185        num_jobs.to_string().into(),
186    ];
187    args.extend(extra_args.into_iter().map(Into::into));
188
189    // Use MAKE passed from the parent process if set. Otherwise prefer `gmake` as it provides a
190    // better feature-wise implementation on some systems.
191    // Notably, we want to avoid SUN make on Solaris (does not support -j) or ancient GNU make 3.81
192    // on MacOS.
193    let inherited = env::var("MAKE");
194    let make_commands: &[&str] = match inherited {
195        Ok(ref x) => &[x.as_str(), "gmake", "make"],
196        _ => &["gmake", "make"],
197    };
198
199    // Give preference to the binary with the name of gmake if it exists because this is typically
200    // the GNU 4+ on MacOS (if it is installed via homebrew).
201    for make in make_commands {
202        /* Use the duct dependency here to merge the output of STDOUT and STDERR into a single stream,
203        and to provide the combined output as a reader which can be iterated over line-by-line. We
204        use duct to do this because it is a lot of work to implement this from scratch. */
205        let result = duct::cmd(*make, &args)
206            .dir(source_dir)
207            .stderr_to_stdout()
208            .run();
209
210        match result {
211            Err(err) if err.kind() == io::ErrorKind::NotFound => {
212                eprintln!("make: command '{make}' not found");
213                continue;
214            }
215            Ok(out) if !out.status.success() => {
216                return Err(io::Error::other(format!(
217                    "make: '{}' failed with {:?}",
218                    make, out.status
219                )));
220            }
221            _ => return result,
222        }
223    }
224
225    Err(io::ErrorKind::NotFound.into())
226}