extern crate duct;
use core::error::Error as StdError;
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::{env, fs};
use flate2::read::GzDecoder;
use tar::Archive;
use crate::verifier::SignatureVerifier;
const NGINX_URL_PREFIX: &str = "https://nginx.org/download";
const OPENSSL_URL_PREFIX: &str = "https://github.com/openssl/openssl/releases/download";
const PCRE1_URL_PREFIX: &str = "https://sourceforge.net/projects/pcre/files/pcre";
const PCRE2_URL_PREFIX: &str = "https://github.com/PCRE2Project/pcre2/releases/download";
const ZLIB_URL_PREFIX: &str = "https://github.com/madler/zlib/releases/download";
const UBUNTU_KEYSEVER: &str = "hkps://keyserver.ubuntu.com";
struct SourceSpec<'a> {
url: fn(&str) -> String,
variable: &'a str,
signature: &'a str,
keyserver: &'a str,
key_ids: &'a [&'a str],
}
const NGINX_SOURCE: SourceSpec = SourceSpec {
url: |version| format!("{NGINX_URL_PREFIX}/nginx-{version}.tar.gz"),
variable: "NGX_VERSION",
signature: "asc",
keyserver: UBUNTU_KEYSEVER,
key_ids: &[
"13C82A63B603576156E30A4EA0EA981B66B0D967",
"D6786CE303D9A9022998DC6CC8464D549AF75C0A",
"B0F4253373F8F6F510D42178520A9993A1C052F8",
"43387825DDB1BB97EC36BA5D007C8D7C15D87369",
],
};
const DEPENDENCIES: &[(&str, SourceSpec)] = &[
(
"openssl",
SourceSpec {
url: |version| {
if version.starts_with("1.") {
let ver_hyphened = version.replace('.', "_");
format!("{OPENSSL_URL_PREFIX}/OpenSSL_{ver_hyphened}/openssl-{version}.tar.gz")
} else {
format!("{OPENSSL_URL_PREFIX}/openssl-{version}/openssl-{version}.tar.gz")
}
},
variable: "OPENSSL_VERSION",
signature: "asc",
keyserver: UBUNTU_KEYSEVER,
key_ids: &[
"EFC0A467D613CB83C7ED6D30D894E2CE8B3D79F5",
"A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C",
"8657ABB260F056B1E5190839D9C4D26D0E604491",
"B7C1C14360F353A36862E4D5231C84CDDCC69C45",
"95A9908DDFA16830BE9FB9003D30A3A9FF1360DC",
"7953AC1FBC3DC8B3B292393ED5E9E43F7DF9EE8C",
"E5E52560DD91C556DDBDA5D02064C53641C25E5D",
"C1F33DD8CE1D4CC613AF14DA9195C48241FBF7DD",
"BA5473A2B0587B07FB27CF2D216094DFD0CB81EF",
],
},
),
(
"pcre",
SourceSpec {
url: |version| {
if version.chars().nth(1).is_some_and(|c| c == '.') {
format!("{PCRE1_URL_PREFIX}/{version}/pcre-{version}.tar.gz")
} else {
format!("{PCRE2_URL_PREFIX}/pcre2-{version}/pcre2-{version}.tar.gz")
}
},
variable: "PCRE2_VERSION",
signature: "sig",
keyserver: UBUNTU_KEYSEVER,
key_ids: &[
"45F68D54BBE23FB3039B46E59766E084FB0F43D8",
"A95536204A3BB489715231282A98E77EB6F24CA8",
],
},
),
(
"zlib",
SourceSpec {
url: |version| format!("{ZLIB_URL_PREFIX}/v{version}/zlib-{version}.tar.gz"),
variable: "ZLIB_VERSION",
signature: "asc",
keyserver: UBUNTU_KEYSEVER,
key_ids: &[
"5ED46A6721D365587791E2AA783FCD8E58BCAFBA",
],
},
),
];
static VERIFIER: LazyLock<Option<SignatureVerifier>> = LazyLock::new(|| {
SignatureVerifier::new()
.inspect_err(|err| eprintln!("GnuPG verifier: {err}"))
.ok()
});
static CACHE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
let base_dir = env::var("OUT_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| env::current_dir().expect("Failed to get current directory"));
let cache_dir = env::var("CACHE_DIR")
.map(PathBuf::from)
.unwrap_or(base_dir.join(".cache"));
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)
.map_err(|err| format!("Failed to create {cache_dir:?}: {err}"))
.unwrap();
}
cache_dir
});
fn download(cache_dir: &Path, url: &str) -> Result<PathBuf, Box<dyn StdError + Send + Sync>> {
fn proceed_with_download(file_path: &Path) -> bool {
!file_path.exists() || file_path.metadata().is_ok_and(|m| m.len() < 1)
}
let filename = url.split('/').next_back().unwrap();
let file_path = cache_dir.join(filename);
if proceed_with_download(&file_path) {
println!("Downloading: {} -> {}", url, file_path.display());
let mut response = ureq::get(url).call()?;
let mut reader = response.body_mut().as_reader();
let mut file = File::create(&file_path)?;
std::io::copy(&mut reader, &mut file)?;
}
if !file_path.exists() {
return Err(
format!("Downloaded file was not written to the expected location: {url}",).into(),
);
}
Ok(file_path)
}
fn get_archive(cache_dir: &Path, source: &SourceSpec, version: &str) -> io::Result<PathBuf> {
let archive_url = (source.url)(version);
let archive = download(cache_dir, &archive_url).map_err(io::Error::other)?;
if let Some(verifier) = &*VERIFIER {
let signature = format!("{archive_url}.{}", source.signature);
let verify = || -> io::Result<()> {
let signature = download(cache_dir, &signature).map_err(io::Error::other)?;
verifier.import_keys(source.keyserver, source.key_ids)?;
verifier.verify_signature(&archive, &signature)?;
Ok(())
};
if let Err(err) = verify() {
let _ = fs::remove_file(&archive);
let _ = fs::remove_file(&signature);
return Err(err);
}
}
Ok(archive)
}
fn extract_archive(archive_path: &Path, extract_output_base_dir: &Path) -> io::Result<PathBuf> {
if !extract_output_base_dir.exists() {
fs::create_dir_all(extract_output_base_dir)?;
}
let archive_file = File::open(archive_path)
.unwrap_or_else(|_| panic!("Unable to open archive file: {}", archive_path.display()));
let stem = archive_path
.file_name()
.and_then(|s| s.to_str())
.and_then(|s| s.rsplitn(3, '.').last())
.expect("Unable to determine archive file name stem");
let extract_output_dir = extract_output_base_dir.to_owned();
let archive_output_dir = extract_output_dir.join(stem);
if !archive_output_dir.exists() {
Archive::new(GzDecoder::new(archive_file))
.entries()?
.filter_map(|e| e.ok())
.for_each(|mut entry| {
let path = entry.path().unwrap();
let stripped_path = path.components().skip(1).collect::<PathBuf>();
entry
.unpack(archive_output_dir.join(stripped_path))
.unwrap();
});
} else {
println!(
"Archive [{}] already extracted to directory: {}",
stem,
archive_output_dir.display()
);
}
Ok(archive_output_dir)
}
pub fn prepare(source_dir: &Path, build_dir: &Path) -> io::Result<(PathBuf, Vec<String>)> {
let extract_output_base_dir = build_dir.join("lib");
if !extract_output_base_dir.exists() {
fs::create_dir_all(&extract_output_base_dir)?;
}
let mut options = vec![];
let source_dir = if let Ok(version) = env::var(NGINX_SOURCE.variable) {
let archive_path = get_archive(&CACHE_DIR, &NGINX_SOURCE, version.as_str())?;
let output_base_dir: PathBuf = env::var("OUT_DIR").unwrap().into();
extract_archive(&archive_path, &output_base_dir)?
} else {
source_dir.to_path_buf()
};
for (name, source) in DEPENDENCIES {
let Ok(requested) = env::var(source.variable) else {
continue;
};
let archive_path = get_archive(&CACHE_DIR, source, &requested)?;
let output_dir = extract_archive(&archive_path, &extract_output_base_dir)?;
let output_dir = output_dir.to_string_lossy();
options.push(format!("--with-{name}={output_dir}"));
}
Ok((source_dir, options))
}