use std::env;
use std::path::{Path, PathBuf};
fn main() {
println!("cargo:rerun-if-changed=native/snapper-pandoc");
println!("cargo:rerun-if-changed=native/snapper-pandoc/build-static.sh");
println!("cargo:rerun-if-env-changed=SNAPPER_PANDOC_LIB_DIR");
println!("cargo:rerun-if-env-changed=SNAPPER_PANDOC_STATIC_LIB");
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_PANDOC_COLINK");
if env::var("CARGO_FEATURE_PANDOC_COLINK").is_err() {
return;
}
let archive = find_static_archive().unwrap_or_else(|| {
panic!(
"pandoc-colink: missing libsnapper_pandoc.a (static build product).\n\
From native/snapper-pandoc run: ./build-static.sh\n\
Or set SNAPPER_PANDOC_STATIC_LIB / SNAPPER_PANDOC_LIB_DIR.\n\
Colink is a build/absorb path — not shared lib + rpath.\n\
See native/snapper-pandoc/README.md for Linux / macOS / Windows notes."
);
});
let archive = archive.canonicalize().unwrap_or(archive);
let archive = {
let s = archive.to_string_lossy();
let stripped = s
.strip_prefix("\\\\?\\UNC\\")
.or_else(|| s.strip_prefix("\\\\?\\"))
.or_else(|| s.strip_prefix("//?/UNC/"))
.or_else(|| s.strip_prefix("//?/"))
.unwrap_or(s.as_ref());
PathBuf::from(stripped)
};
let target = env::var("TARGET").unwrap_or_else(|_| env::var("HOST").unwrap_or_default());
if target.contains("apple-darwin") || target.contains("apple-ios") {
emit_darwin(&archive);
} else if target.contains("windows") {
println!(
"cargo:warning=pandoc-colink on Windows is unsupported in CI; \
prefer cabal foreign-library snapper_pandoc.dll + features cli,pandoc \
(see native/snapper-pandoc/README.md)"
);
emit_windows(&archive);
} else {
emit_linux(&archive);
}
println!("cargo:rustc-cfg=pandoc_colink");
println!(
"cargo:warning=pandoc-colink: static absorb {} for target {target}",
archive.display()
);
}
fn link_arg(arg: &str) {
println!("cargo:rustc-link-arg-bins={arg}");
}
fn emit_linux(archive: &Path) {
println!("cargo:rustc-link-arg-bins=-no-pie");
link_arg("-fuse-ld=bfd");
link_arg("-Wl,--gc-sections");
link_arg("-Wl,--no-as-needed");
link_arg("-Wl,--start-group");
link_arg(&archive.display().to_string());
for lib in [
"m", "z", "gmp", "ffi", "bz2", "lzma", "zstd", "elf", "dw", "numa", "pthread", "dl", "rt",
"c",
] {
link_arg(&format!("-l{lib}"));
}
link_arg("-Wl,--end-group");
link_arg("-L/usr/lib");
link_arg("-L/usr/lib64");
}
fn emit_darwin(archive: &Path) {
link_arg("-Wl,-dead_strip");
link_arg(&format!("-Wl,-force_load,{}", archive.display()));
for lib in ["m", "z", "iconv", "System", "pthread", "dl", "c"] {
link_arg(&format!("-l{lib}"));
}
for fw in ["Security", "CoreFoundation", "SystemConfiguration"] {
link_arg("-framework");
link_arg(fw);
}
for dir in [
"/opt/homebrew/lib",
"/usr/local/lib",
"/opt/local/lib",
"/opt/homebrew/opt/gmp/lib",
"/opt/homebrew/opt/libffi/lib",
] {
if Path::new(dir).is_dir() {
link_arg(&format!("-L{dir}"));
}
}
for lib in ["gmp", "ffi"] {
link_arg(&format!("-l{lib}"));
}
}
fn win_mingw_path(p: &str) -> PathBuf {
let p = p.trim();
if p.len() >= 3 {
let bytes = p.as_bytes();
if bytes[0] == b'/' && bytes[1].is_ascii_alphabetic() && bytes[2] == b'/' {
let drive = (bytes[1] as char).to_ascii_uppercase();
return PathBuf::from(format!("{drive}:/{}", &p[3..]));
}
}
PathBuf::from(p)
}
fn split_mingw_lib_dirs(extra: &str) -> Vec<PathBuf> {
let mut out = Vec::new();
for part in extra.split([';', '\n', '\r']) {
let part = part.trim();
if !part.is_empty() {
out.push(win_mingw_path(part));
}
}
out
}
fn find_named_lib(dirs: &[PathBuf], names: &[&str]) -> Option<PathBuf> {
for d in dirs {
for name in names {
let p = d.join(name);
if p.is_file() {
return Some(p);
}
}
}
None
}
fn emit_windows(archive: &Path) {
let s = archive.display().to_string().replace('\\', "/");
let is_gnu = env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu");
let is_msvc = env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc");
println!("cargo:rerun-if-env-changed=SNAPPER_MINGW_BIN");
println!("cargo:rerun-if-env-changed=SNAPPER_GHC_REAL");
println!("cargo:rerun-if-env-changed=SNAPPER_MINGW_LIB");
println!("cargo:rerun-if-env-changed=SNAPPER_MINGW_GMP");
println!("cargo:rerun-if-env-changed=SNAPPER_MINGW_FFI");
let mut lib_dirs: Vec<PathBuf> = Vec::new();
if let Ok(extra) = env::var("SNAPPER_MINGW_LIB") {
lib_dirs.extend(split_mingw_lib_dirs(&extra));
}
if let Ok(bin) = env::var("SNAPPER_MINGW_BIN") {
let bin = win_mingw_path(&bin);
lib_dirs.push(bin.join("..").join("lib"));
lib_dirs.push(bin.join("..").join("x86_64-w64-mingw32").join("lib"));
}
if let Ok(ghc) = env::var("SNAPPER_GHC_REAL") {
let ghc = win_mingw_path(&ghc);
if let Some(root) = ghc.parent().and_then(|p| p.parent()) {
lib_dirs.push(root.join("mingw").join("lib"));
lib_dirs.push(root.join("mingw").join("x86_64-w64-mingw32").join("lib"));
lib_dirs.push(root.join("lib"));
}
}
for d in [
r"C:\msys64\mingw64\lib",
r"C:\mingw64\lib",
r"C:\ProgramData\mingw64\mingw64\lib",
r"C:\tools\msys64\mingw64\lib",
"/c/msys64/mingw64/lib",
"/c/mingw64/lib",
"/mingw64/lib",
] {
lib_dirs.push(win_mingw_path(d));
}
let gmp_abs = env::var("SNAPPER_MINGW_GMP")
.ok()
.map(|p| win_mingw_path(&p))
.filter(|p| p.is_file())
.or_else(|| find_named_lib(&lib_dirs, &["libgmp.a", "gmp.lib"]));
let ffi_abs = env::var("SNAPPER_MINGW_FFI")
.ok()
.map(|p| win_mingw_path(&p))
.filter(|p| p.is_file())
.or_else(|| find_named_lib(&lib_dirs, &["libffi.a", "ffi.lib"]));
for d in &lib_dirs {
if d.is_dir() {
let p = d.display().to_string().replace('\\', "/");
link_arg(&format!("-L{p}"));
}
}
if let Some(ref g) = gmp_abs {
println!(
"cargo:warning=pandoc-colink: gmp archive {}",
g.display().to_string().replace('\\', "/")
);
}
if let Some(ref f) = ffi_abs {
println!(
"cargo:warning=pandoc-colink: ffi archive {}",
f.display().to_string().replace('\\', "/")
);
}
if s.ends_with(".lib") {
link_arg(&s);
} else if is_gnu {
link_arg("-Wl,--allow-multiple-definition");
link_arg("-Wl,--start-group");
link_arg(&s);
if let Some(g) = gmp_abs {
link_arg(&g.display().to_string().replace('\\', "/"));
} else {
link_arg("-lgmp");
}
if let Some(f) = ffi_abs {
link_arg(&f.display().to_string().replace('\\', "/"));
} else {
link_arg("-lffi");
}
for lib in [
"z", "ws2_32", "user32", "shell32", "gdi32", "advapi32", "kernel32", "ole32",
"oleaut32", "rpcrt4", "uuid", "winmm", "dbghelp", "ntdll", "pthread",
] {
link_arg(&format!("-l{lib}"));
}
link_arg("-Wl,--end-group");
link_arg("-lmingwex");
link_arg("-lmsvcrt");
} else {
link_arg(&s);
link_arg("-Wl,--allow-multiple-definition");
}
if is_msvc {
println!(
"cargo:warning=pandoc-colink on windows-msvc: ensure the static archive \
was built for MSVC or use windows-gnu; mingw .a often fails to link"
);
}
}
fn find_static_archive() -> Option<PathBuf> {
if let Ok(p) = env::var("SNAPPER_PANDOC_STATIC_LIB") {
let path = PathBuf::from(p);
if path.is_file() {
return Some(path);
}
}
if let Ok(d) = env::var("SNAPPER_PANDOC_LIB_DIR") {
let dir = PathBuf::from(&d);
for name in [
"libsnapper_pandoc.a",
"snapper_pandoc.lib",
"libsnapper_pandoc.lib",
] {
let a = dir.join(name);
if a.is_file() {
return Some(a);
}
}
}
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
for c in [
manifest.join("native/snapper-pandoc/lib/libsnapper_pandoc.a"),
manifest.join("native/snapper-pandoc/lib/snapper_pandoc.lib"),
manifest.join("native/snapper-pandoc/static-build/libsnapper_pandoc.a"),
] {
if c.is_file() {
return Some(c);
}
}
let dist = manifest.join("native/snapper-pandoc/dist-newstyle");
if dist.is_dir() {
if let Some(found) = find_file(&dist, "libsnapper_pandoc.a") {
return Some(found);
}
}
None
}
fn find_file(root: &Path, name: &str) -> Option<PathBuf> {
fn walk(dir: &Path, name: &str, depth: usize) -> Option<PathBuf> {
if depth > 10 {
return None;
}
for ent in std::fs::read_dir(dir).ok()?.flatten() {
let p = ent.path();
if p.is_file() && p.file_name().and_then(|s| s.to_str()) == Some(name) {
return Some(p);
}
if p.is_dir() {
if let Some(f) = walk(&p, name, depth + 1) {
return Some(f);
}
}
}
None
}
walk(root, name, 0)
}