nginx_src/
download.rs

1extern crate duct;
2
3use std::error::Error as StdError;
4use std::fs::File;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::sync::LazyLock;
8use std::{env, fs};
9
10use flate2::read::GzDecoder;
11use tar::Archive;
12
13use crate::verifier::SignatureVerifier;
14
15const NGINX_URL_PREFIX: &str = "https://nginx.org/download";
16const OPENSSL_URL_PREFIX: &str = "https://github.com/openssl/openssl/releases/download";
17const PCRE1_URL_PREFIX: &str = "https://sourceforge.net/projects/pcre/files/pcre";
18const PCRE2_URL_PREFIX: &str = "https://github.com/PCRE2Project/pcre2/releases/download";
19const ZLIB_URL_PREFIX: &str = "https://github.com/madler/zlib/releases/download";
20const UBUNTU_KEYSEVER: &str = "hkps://keyserver.ubuntu.com";
21
22struct SourceSpec<'a> {
23    url: fn(&str) -> String,
24    variable: &'a str,
25    signature: &'a str,
26    keyserver: &'a str,
27    key_ids: &'a [&'a str],
28}
29
30const NGINX_SOURCE: SourceSpec = SourceSpec {
31    url: |version| format!("{NGINX_URL_PREFIX}/nginx-{version}.tar.gz"),
32    variable: "NGX_VERSION",
33    signature: "asc",
34    keyserver: UBUNTU_KEYSEVER,
35    key_ids: &[
36        // Key 1: Konstantin Pavlov's public key. For Nginx 1.25.3 and earlier
37        "13C82A63B603576156E30A4EA0EA981B66B0D967",
38        // Key 2: Sergey Kandaurov's public key. For Nginx 1.25.4
39        "D6786CE303D9A9022998DC6CC8464D549AF75C0A",
40        // Key 3: Maxim Dounin's public key. At least used for Nginx 1.18.0
41        "B0F4253373F8F6F510D42178520A9993A1C052F8",
42        // Key 4: Roman Arutyunyan's public key. For Nginx 1.25.5
43        "43387825DDB1BB97EC36BA5D007C8D7C15D87369",
44    ],
45};
46
47const DEPENDENCIES: &[(&str, SourceSpec)] = &[
48    (
49        "openssl",
50        SourceSpec {
51            url: |version| {
52                if version.starts_with("1.") {
53                    let ver_hyphened = version.replace('.', "_");
54                    format!("{OPENSSL_URL_PREFIX}/OpenSSL_{ver_hyphened}/openssl-{version}.tar.gz")
55                } else {
56                    format!("{OPENSSL_URL_PREFIX}/openssl-{version}/openssl-{version}.tar.gz")
57                }
58            },
59            variable: "OPENSSL_VERSION",
60            signature: "asc",
61            keyserver: UBUNTU_KEYSEVER,
62            key_ids: &[
63                "EFC0A467D613CB83C7ED6D30D894E2CE8B3D79F5",
64                "A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C",
65                "8657ABB260F056B1E5190839D9C4D26D0E604491",
66                "B7C1C14360F353A36862E4D5231C84CDDCC69C45",
67                "95A9908DDFA16830BE9FB9003D30A3A9FF1360DC",
68                "7953AC1FBC3DC8B3B292393ED5E9E43F7DF9EE8C",
69                "E5E52560DD91C556DDBDA5D02064C53641C25E5D",
70                "C1F33DD8CE1D4CC613AF14DA9195C48241FBF7DD",
71                "BA5473A2B0587B07FB27CF2D216094DFD0CB81EF",
72            ],
73        },
74    ),
75    (
76        "pcre",
77        SourceSpec {
78            url: |version| {
79                // We can distinguish pcre1/pcre2 by checking whether the second character is '.',
80                // because the final version of pcre1 is 8.45 and the first one of pcre2 is 10.00.
81                if version.chars().nth(1).is_some_and(|c| c == '.') {
82                    format!("{PCRE1_URL_PREFIX}/{version}/pcre-{version}.tar.gz")
83                } else {
84                    format!("{PCRE2_URL_PREFIX}/pcre2-{version}/pcre2-{version}.tar.gz")
85                }
86            },
87            variable: "PCRE2_VERSION",
88            signature: "sig",
89            keyserver: UBUNTU_KEYSEVER,
90            key_ids: &[
91                // Key 1: Phillip Hazel's public key. For PCRE2 10.44 and earlier
92                "45F68D54BBE23FB3039B46E59766E084FB0F43D8",
93                // Key 2: Nicholas Wilson's public key. For PCRE2 10.45
94                "A95536204A3BB489715231282A98E77EB6F24CA8",
95            ],
96        },
97    ),
98    (
99        "zlib",
100        SourceSpec {
101            url: |version| format!("{ZLIB_URL_PREFIX}/v{version}/zlib-{version}.tar.gz"),
102            variable: "ZLIB_VERSION",
103            signature: "asc",
104            keyserver: UBUNTU_KEYSEVER,
105            key_ids: &[
106                // Key 1: Mark Adler's public key. For zlib 1.3.1 and earlier
107                "5ED46A6721D365587791E2AA783FCD8E58BCAFBA",
108            ],
109        },
110    ),
111];
112
113static VERIFIER: LazyLock<Option<SignatureVerifier>> = LazyLock::new(|| {
114    SignatureVerifier::new()
115        .inspect_err(|err| eprintln!("GnuPG verifier: {err}"))
116        .ok()
117});
118
119fn make_cache_dir() -> io::Result<PathBuf> {
120    let base_dir = env::var("CARGO_MANIFEST_DIR")
121        .map(PathBuf::from)
122        .unwrap_or_else(|_| env::current_dir().expect("Failed to get current directory"));
123    // Choose `.cache` relative to the manifest directory (nginx-src) as the default cache directory
124    // Environment variable `CACHE_DIR` overrides this
125    // Recommendation: set env "CACHE_DIR = { value = ".cache", relative = true }" in
126    // `.cargo/config.toml` in your project
127    let cache_dir = env::var("CACHE_DIR")
128        .map(PathBuf::from)
129        .unwrap_or(base_dir.join(".cache"));
130    if !cache_dir.exists() {
131        fs::create_dir_all(&cache_dir)?;
132    }
133    Ok(cache_dir)
134}
135
136/// Downloads a tarball from the specified URL into the `.cache` directory.
137fn download(cache_dir: &Path, url: &str) -> Result<PathBuf, Box<dyn StdError + Send + Sync>> {
138    fn proceed_with_download(file_path: &Path) -> bool {
139        // File does not exist or is zero bytes
140        !file_path.exists() || file_path.metadata().is_ok_and(|m| m.len() < 1)
141    }
142    let filename = url.split('/').next_back().unwrap();
143    let file_path = cache_dir.join(filename);
144    if proceed_with_download(&file_path) {
145        println!("Downloading: {} -> {}", url, file_path.display());
146        let mut response = ureq::get(url).call()?;
147        let mut reader = response.body_mut().as_reader();
148        let mut file = File::create(&file_path)?;
149        std::io::copy(&mut reader, &mut file)?;
150    }
151
152    if !file_path.exists() {
153        return Err(
154            format!("Downloaded file was not written to the expected location: {url}",).into(),
155        );
156    }
157    Ok(file_path)
158}
159
160/// Gets a given tarball and signature file from a remote URL and copies it to the `.cache`
161/// directory.
162fn get_archive(cache_dir: &Path, source: &SourceSpec, version: &str) -> io::Result<PathBuf> {
163    let archive_url = (source.url)(version);
164    let archive = download(cache_dir, &archive_url).map_err(io::Error::other)?;
165
166    if let Some(verifier) = &*VERIFIER {
167        let signature = format!("{archive_url}.{}", source.signature);
168
169        let verify = || -> io::Result<()> {
170            let signature = download(cache_dir, &signature).map_err(io::Error::other)?;
171            verifier.import_keys(source.keyserver, source.key_ids)?;
172            verifier.verify_signature(&archive, &signature)?;
173            Ok(())
174        };
175
176        if let Err(err) = verify() {
177            let _ = fs::remove_file(&archive);
178            let _ = fs::remove_file(&signature);
179            return Err(err);
180        }
181    }
182
183    Ok(archive)
184}
185
186/// Extracts a tarball into a subdirectory based on the tarball's name under the source base
187/// directory.
188fn extract_archive(archive_path: &Path, extract_output_base_dir: &Path) -> io::Result<PathBuf> {
189    if !extract_output_base_dir.exists() {
190        fs::create_dir_all(extract_output_base_dir)?;
191    }
192    let archive_file = File::open(archive_path)
193        .unwrap_or_else(|_| panic!("Unable to open archive file: {}", archive_path.display()));
194    let stem = archive_path
195        .file_name()
196        .and_then(|s| s.to_str())
197        .and_then(|s| s.rsplitn(3, '.').last())
198        .expect("Unable to determine archive file name stem");
199
200    let extract_output_dir = extract_output_base_dir.to_owned();
201    let archive_output_dir = extract_output_dir.join(stem);
202    if !archive_output_dir.exists() {
203        Archive::new(GzDecoder::new(archive_file))
204            .entries()?
205            .filter_map(|e| e.ok())
206            .for_each(|mut entry| {
207                let path = entry.path().unwrap();
208                let stripped_path = path.components().skip(1).collect::<PathBuf>();
209                entry
210                    .unpack(archive_output_dir.join(stripped_path))
211                    .unwrap();
212            });
213    } else {
214        println!(
215            "Archive [{}] already extracted to directory: {}",
216            stem,
217            archive_output_dir.display()
218        );
219    }
220
221    Ok(archive_output_dir)
222}
223
224/// Downloads and extracts all requested sources.
225pub fn prepare(source_dir: &Path, build_dir: &Path) -> io::Result<(PathBuf, Vec<String>)> {
226    let extract_output_base_dir = build_dir.join("lib");
227    if !extract_output_base_dir.exists() {
228        fs::create_dir_all(&extract_output_base_dir)?;
229    }
230
231    let cache_dir = make_cache_dir()?;
232    let mut options = vec![];
233
234    // Download NGINX only if NGX_VERSION is set.
235    let source_dir = if let Ok(version) = env::var(NGINX_SOURCE.variable) {
236        let archive_path = get_archive(&cache_dir, &NGINX_SOURCE, version.as_str())?;
237        let output_base_dir: PathBuf = env::var("OUT_DIR").unwrap().into();
238        extract_archive(&archive_path, &output_base_dir)?
239    } else {
240        source_dir.to_path_buf()
241    };
242
243    for (name, source) in DEPENDENCIES {
244        // Download dependencies if a corresponding DEPENDENCY_VERSION is set.
245        let Ok(requested) = env::var(source.variable) else {
246            continue;
247        };
248
249        let archive_path = get_archive(&cache_dir, source, &requested)?;
250        let output_dir = extract_archive(&archive_path, &extract_output_base_dir)?;
251        let output_dir = output_dir.to_string_lossy();
252        options.push(format!("--with-{name}={output_dir}"));
253    }
254
255    Ok((source_dir, options))
256}