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 "13C82A63B603576156E30A4EA0EA981B66B0D967",
38 "D6786CE303D9A9022998DC6CC8464D549AF75C0A",
40 "B0F4253373F8F6F510D42178520A9993A1C052F8",
42 "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 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 "45F68D54BBE23FB3039B46E59766E084FB0F43D8",
93 "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 "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 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
136fn download(cache_dir: &Path, url: &str) -> Result<PathBuf, Box<dyn StdError + Send + Sync>> {
138 fn proceed_with_download(file_path: &Path) -> bool {
139 !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
160fn 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
186fn 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
224pub 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 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 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}