use std::env;
use std::path::PathBuf;
use std::process;
const PREBUILT: &str = include_str!("prebuilt.toml");
const ARCHIVE: &str = "libneedle.a";
fn main() {
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed=prebuilt.toml");
println!("cargo::rerun-if-env-changed=CACTUS_NEEDLE_LIB_DIR");
println!("cargo::rerun-if-env-changed=CACTUS_CXXSTDLIB");
println!("cargo::rerun-if-env-changed=DOCS_RS");
println!(
"cargo::rustc-env=NEEDLE_ENGINE_COMMIT={}",
prebuilt("commit")
);
if !feature("NEEDLE") {
return;
}
if env::var_os("DOCS_RS").is_some() {
return;
}
let lib_dir = match env::var_os("CACTUS_NEEDLE_LIB_DIR") {
Some(dir) => lib_dir_from_env(PathBuf::from(dir)),
None => downloaded_lib_dir(),
};
println!("cargo::rustc-link-search=native={}", lib_dir.display());
println!("cargo::rustc-link-lib=static=needle");
if cfg("CARGO_CFG_TARGET_OS") == "linux" {
libcxx_compat();
}
for lib in cxx_runtime() {
println!("cargo::rustc-link-lib={lib}");
}
}
fn fail(message: &str) -> ! {
for line in message.lines() {
println!("cargo::error={line}");
}
process::exit(1);
}
fn feature(name: &str) -> bool {
env::var_os(format!("CARGO_FEATURE_{name}")).is_some()
}
fn cfg(name: &str) -> String {
env::var(name).unwrap_or_default()
}
fn prebuilt(key: &str) -> &'static str {
let found = PREBUILT.lines().find_map(|line| {
let line = line.trim();
if line.starts_with('#') {
return None;
}
let (name, value) = line.split_once('=')?;
(name.trim() == key).then(|| value.trim().trim_matches('"'))
});
found.unwrap_or_else(|| {
fail(&format!(
"cactus-sys: prebuilt.toml has no entry for `{key}`.\n\
The pinned release table is incomplete or was edited by hand.\n\
Help: restore cactus-sys/prebuilt.toml from the repository."
))
})
}
fn lib_dir_from_env(dir: PathBuf) -> PathBuf {
if dir.join(ARCHIVE).is_file() {
return dir;
}
fail(&format!(
"cactus-sys: CACTUS_NEEDLE_LIB_DIR is set to `{}`, but `{ARCHIVE}` is not in it.\n\
The variable must name the directory that contains the archive, not the archive itself.\n\
Help: point it at a directory holding {ARCHIVE}, or unset it and build with the\n\
`download-binaries` feature to fetch the pinned archive automatically.",
dir.display()
))
}
#[cfg(not(feature = "download-binaries"))]
fn downloaded_lib_dir() -> PathBuf {
fail(
"cactus-sys: no engine archive available.\n\
The `needle` feature is on, but `download-binaries` is off and CACTUS_NEEDLE_LIB_DIR\n\
is unset, so there is nothing to link against. Cactus Compute ships Needle as a\n\
closed-source static library; this crate cannot build one from source.\n\
Help: either set CACTUS_NEEDLE_LIB_DIR to a directory containing libneedle.a for your\n\
target, or enable the `download-binaries` feature to fetch the pinned, SHA-256 verified\n\
archive from Hugging Face.",
)
}
#[cfg(feature = "download-binaries")]
fn downloaded_lib_dir() -> PathBuf {
let platform = platform();
let expected = prebuilt(&platform);
let out_dir = match env::var_os("OUT_DIR") {
Some(dir) => PathBuf::from(dir).join(&platform),
None => fail(
"cactus-sys: OUT_DIR is unset.\n\
Build scripts are always given one, so this build was not started by Cargo.\n\
Help: build the crate with `cargo build`.",
),
};
let archive = out_dir.join(ARCHIVE);
if download::digest(&archive).as_deref() == Some(expected) {
return out_dir;
}
let url = format!(
"https://huggingface.co/{repo}/resolve/{commit}/{platform}/{ARCHIVE}",
repo = prebuilt("repo"),
commit = prebuilt("commit"),
);
download::fetch(&url, &out_dir, &archive, expected);
out_dir
}
#[cfg(feature = "download-binaries")]
fn platform() -> String {
let os = cfg("CARGO_CFG_TARGET_OS");
let arch = cfg("CARGO_CFG_TARGET_ARCH");
let abi = cfg("CARGO_CFG_TARGET_ABI");
let target_env = cfg("CARGO_CFG_TARGET_ENV");
let endian = cfg("CARGO_CFG_TARGET_ENDIAN");
let platform = match (os.as_str(), arch.as_str()) {
("macos", "aarch64") => "macos-arm64",
("macos", "x86_64") => fail(
"cactus-sys: Intel macOS is not supported.\n\
Cactus Compute publishes no libneedle.a for x86_64-apple-darwin; the only macOS\n\
archive upstream ships is arm64.\n\
Help: build for aarch64-apple-darwin, or set CACTUS_NEEDLE_LIB_DIR to a directory\n\
holding an x86_64 libneedle.a you built or obtained yourself.",
),
("linux", _) if target_env == "gnu" || target_env == "musl" => {
match (arch.as_str(), endian.as_str()) {
("x86_64", _) => "linux-x86_64",
("aarch64", _) => "linux-arm64",
("arm", _) => "linux-armv7",
("riscv64", _) => "linux-riscv64",
("mips", "little") => "linux-mipsel",
_ => fail(&unsupported(&os, &arch)),
}
}
("android", "aarch64") => "android-arm64",
("android", "arm") => "android-armv7",
("android", "riscv64") => "android-riscv64",
("ios", "aarch64") if abi == "sim" => "ios-sim-arm64",
("ios", "aarch64") if abi.is_empty() => "ios-arm64",
("tvos", "aarch64") => "tvos-arm64",
("watchos", "aarch64") => "watchos-arm64",
("windows", _) if target_env == "msvc" => fail(
"cactus-sys: MSVC Windows targets are not supported.\n\
The upstream Windows archives are llvm-mingw builds: they use the Itanium C++ ABI\n\
and LLVM libc++, which the MSVC toolchain cannot link.\n\
Help: build for x86_64-pc-windows-gnullvm or aarch64-pc-windows-gnullvm.",
),
("windows", _) if abi != "llvm" => fail(
"cactus-sys: GNU (mingw-w64) Windows targets are not supported.\n\
The upstream Windows archives are llvm-mingw builds against LLVM libc++, not the\n\
libstdc++ that the *-pc-windows-gnu targets link.\n\
Help: build for x86_64-pc-windows-gnullvm or aarch64-pc-windows-gnullvm.",
),
("windows", "x86_64") => "windows-x86_64",
("windows", "aarch64") => "windows-arm64",
_ => fail(&unsupported(&os, &arch)),
};
platform.to_owned()
}
#[cfg(feature = "download-binaries")]
fn unsupported(os: &str, arch: &str) -> String {
format!(
"cactus-sys: no prebuilt Needle engine for this target (os `{os}`, arch `{arch}`).\n\
Cactus Compute publishes archives for macOS arm64, Linux (x86_64, arm64, armv7,\n\
riscv64, mipsel), Android (arm64, armv7, riscv64), iOS, iOS Simulator, tvOS, watchOS\n\
and llvm-mingw Windows (x86_64, arm64).\n\
Help: build for one of those targets, or set CACTUS_NEEDLE_LIB_DIR to a directory\n\
holding a libneedle.a for this one."
)
}
fn libcxx_compat() {
println!("cargo::rerun-if-changed=shim/libcxx_compat.c");
cc::Build::new()
.file("shim/libcxx_compat.c")
.warnings(true)
.compile("cactus_libcxx_compat");
}
fn cxx_runtime() -> Vec<String> {
if let Ok(list) = env::var("CACTUS_CXXSTDLIB") {
return list
.split(',')
.map(str::trim)
.filter(|lib| !lib.is_empty())
.map(|lib| match lib.split_once('=') {
Some(_) => lib.to_owned(),
None => format!("dylib={lib}"),
})
.collect();
}
let libs: &[&str] = match cfg("CARGO_CFG_TARGET_OS").as_str() {
"macos" | "ios" | "tvos" | "watchos" | "visionos" => &["c++"],
"linux" => {
require_libcxx();
&["c++", "c++abi"]
}
"android" => &["c++_shared"],
"windows" => &["c++"],
_ => &[],
};
libs.iter().map(|lib| format!("dylib={lib}")).collect()
}
fn require_libcxx() {
if env::var("HOST").ok() != env::var("TARGET").ok() {
return;
}
let compiler = env::var("CC").unwrap_or_else(|_| "cc".to_owned());
let found = |library: &str| {
let output = process::Command::new(&compiler)
.arg(format!("-print-file-name={library}"))
.output()
.ok()?;
Some(String::from_utf8_lossy(&output.stdout).trim().contains('/'))
};
let (Some(shared), Some(archive)) = (found("libc++.so"), found("libc++.a")) else {
return;
};
if shared || archive {
return;
}
fail(
"cactus-sys: LLVM libc++ was not found.\n\
The Needle engine archive is built against libc++, and this system appears to have only\n\
GNU libstdc++, which cannot stand in for it.\n\
Help: install libc++ (Debian and Ubuntu: `sudo apt install libc++-dev libc++abi-dev`;\n\
Fedora: `sudo dnf install libcxx-devel libcxxabi-devel`), or set CACTUS_CXXSTDLIB to the\n\
libraries to link instead.",
)
}
#[cfg(feature = "download-binaries")]
mod download {
use std::fmt::Write as _;
use std::fs;
use std::io::{self, Read as _};
use std::path::Path;
use sha2::{Digest as _, Sha256};
pub fn digest(path: &Path) -> Option<String> {
let mut file = fs::File::open(path).ok()?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).ok()?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
let mut hex = String::with_capacity(64);
for byte in hasher.finalize() {
write!(hex, "{byte:02x}").expect("writing to a String cannot fail");
}
Some(hex)
}
pub fn fetch(url: &str, out_dir: &Path, archive: &Path, expected: &str) {
if let Err(error) = fs::create_dir_all(out_dir) {
super::fail(&format!(
"cactus-sys: could not create `{}`: {error}\n\
Help: check the permissions on the Cargo target directory.",
out_dir.display()
));
}
let partial = archive.with_extension("a.partial");
get(url, &partial);
let actual = digest(&partial).unwrap_or_default();
if actual != expected {
let _ = fs::remove_file(&partial);
super::fail(&format!(
"cactus-sys: checksum mismatch for the downloaded engine archive.\n\
url: {url}\n\
expected: {expected}\n\
actual: {actual}\n\
The download was discarded. Either the transfer was corrupted, a proxy served\n\
something else, or prebuilt.toml no longer matches the pinned commit.\n\
Help: retry the build; if it keeps failing, verify cactus-sys/prebuilt.toml\n\
against the upstream repository before trusting the archive."
));
}
if let Err(error) = fs::rename(&partial, archive) {
let _ = fs::remove_file(&partial);
super::fail(&format!(
"cactus-sys: could not install `{}`: {error}\n\
Help: remove the file and rebuild.",
archive.display()
));
}
}
fn get(url: &str, dest: &Path) {
const MAX_BODY: u64 = 64 * 1024 * 1024;
let config = ureq::Agent::config_builder()
.user_agent(concat!("cactus-sys/", env!("CARGO_PKG_VERSION")))
.build();
let agent = ureq::Agent::new_with_config(config);
let mut response = match agent.get(url).call() {
Ok(response) => response,
Err(error) => super::fail(&format!(
"cactus-sys: could not download the Needle engine archive.\n\
url: {url}\n\
error: {error}\n\
Help: check network access to huggingface.co, or build offline by setting\n\
CACTUS_NEEDLE_LIB_DIR to a directory holding libneedle.a for your target."
)),
};
let mut file = match fs::File::create(dest) {
Ok(file) => file,
Err(error) => super::fail(&format!(
"cactus-sys: could not write `{}`: {error}\n\
Help: check the free space and permissions on the Cargo target directory.",
dest.display()
)),
};
let mut body = response.body_mut().with_config().limit(MAX_BODY).reader();
if let Err(error) = io::copy(&mut body, &mut file) {
let _ = fs::remove_file(dest);
super::fail(&format!(
"cactus-sys: the engine archive download failed part way through.\n\
url: {url}\n\
error: {error}\n\
Help: retry the build."
));
}
}
}