printwell-sys 0.1.11

Low-level FFI bindings for Printwell using cxx
Documentation
//! Build script for printwell-sys.
//!
//! Links against `libprintwell_native.so` (shared library with C++ + Chromium).
//!
//! Search order for the native library:
//! 1. `PRINTWELL_NATIVE_DIR` env var (explicit override)
//! 2. `OUT_DIR` (downloaded from GitHub releases)
//! 3. target/native/ (built by xtask)
//! 4. native/linux-x64/ (pre-built from Git LFS for contributors)

use flate2::read::GzDecoder;
use std::env;
use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use tar::Archive;

const GITHUB_REPO: &str = "printwell-dev/core";
const LIB_NAME: &str = "printwell_native";

fn main() {
    let target = env::var("TARGET").unwrap_or_else(|_| "x86_64-unknown-linux-gnu".to_string());
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let workspace_root = Path::new(&manifest_dir).parent().unwrap().parent().unwrap();
    let version = env!("CARGO_PKG_VERSION");

    // Find or download the native library
    let native_dir = find_or_download_native_lib(&target, &out_dir, workspace_root, version);

    println!(
        "cargo:warning=Using native library from: {}",
        native_dir.display()
    );
    println!("cargo:rustc-link-search=native={}", native_dir.display());
    println!("cargo:rustc-link-lib=dylib={LIB_NAME}");

    // Set rpath so the binary can find the .so at runtime
    #[cfg(target_os = "linux")]
    {
        println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
        println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/../lib");
        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", native_dir.display());
    }

    #[cfg(target_os = "macos")]
    {
        println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
        println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../lib");
        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", native_dir.display());
    }

    let lib_file = native_dir.join(lib_filename(&target));
    println!("cargo:rerun-if-changed={}", lib_file.display());
    println!("cargo:rerun-if-env-changed=PRINTWELL_NATIVE_DIR");
    println!("cargo:rerun-if-env-changed=PRINTWELL_OFFLINE");
}

fn find_or_download_native_lib(
    target: &str,
    out_dir: &Path,
    workspace_root: &Path,
    version: &str,
) -> PathBuf {
    let lib_file = lib_filename(target);

    // 1. Explicit override via env var
    if let Ok(dir) = env::var("PRINTWELL_NATIVE_DIR") {
        let path = Path::new(&dir).join(&lib_file);
        if path.exists() {
            return PathBuf::from(dir);
        }
        println!(
            "cargo:warning=PRINTWELL_NATIVE_DIR set but library not found at {}",
            path.display()
        );
    }

    // 2. Already downloaded to OUT_DIR
    let out_lib = out_dir.join(&lib_file);
    if out_lib.exists() {
        return out_dir.to_path_buf();
    }

    // 3. Built by xtask in target/native/
    let target_native = workspace_root.join("target/native").join(&lib_file);
    if target_native.exists() {
        return workspace_root.join("target/native");
    }

    // 4. Pre-built from Git LFS (platform-specific directories)
    let platform_dir = platform_dir(target);
    let prebuilt = workspace_root
        .join("native")
        .join(platform_dir)
        .join(&lib_file);
    if prebuilt.exists() {
        return workspace_root.join("native").join(platform_dir);
    }

    // 5. Try to download from GitHub releases (unless offline mode)
    if env::var("PRINTWELL_OFFLINE").is_err()
        && let Some(downloaded_dir) = download_native_lib(target, out_dir, version)
    {
        return downloaded_dir;
    }

    // Not found - provide helpful error message
    println!("cargo:warning=Native library not found!");
    println!("cargo:warning=Options:");
    println!("cargo:warning=  1. Run `cargo xtask build` to build libprintwell_native");
    println!("cargo:warning=  2. Run `git lfs pull` to fetch pre-built library");
    println!("cargo:warning=  3. Set PRINTWELL_NATIVE_DIR to the library location");
    println!("cargo:warning=  4. Ensure network access for automatic download");

    // Return OUT_DIR anyway - linker will give a clear error
    out_dir.to_path_buf()
}

fn download_native_lib(target: &str, out_dir: &Path, version: &str) -> Option<PathBuf> {
    let archive_name = format!("libprintwell_native-{target}.tar.gz");
    let url =
        format!("https://github.com/{GITHUB_REPO}/releases/download/v{version}/{archive_name}");

    println!("cargo:warning=Downloading native library from {url}");

    let response = match ureq::get(&url).call() {
        Ok(r) => r,
        Err(e) => {
            println!("cargo:warning=Failed to download: {e}");
            return None;
        }
    };

    if response.status() != 200 {
        println!(
            "cargo:warning=Download failed with status {}",
            response.status()
        );
        return None;
    }

    // Download to temp file
    let archive_path = out_dir.join(&archive_name);
    let (_, body) = response.into_parts();
    let mut reader = body.into_reader();
    let file = match File::create(&archive_path) {
        Ok(f) => f,
        Err(e) => {
            println!("cargo:warning=Failed to create archive file: {e}");
            return None;
        }
    };
    let mut writer = BufWriter::new(file);

    if let Err(e) = io::copy(&mut reader, &mut writer) {
        println!("cargo:warning=Failed to write archive: {e}");
        return None;
    }
    writer.flush().ok();
    drop(writer);

    // Extract archive
    let archive_file = match File::open(&archive_path) {
        Ok(f) => f,
        Err(e) => {
            println!("cargo:warning=Failed to open archive: {e}");
            return None;
        }
    };
    let decoder = GzDecoder::new(BufReader::new(archive_file));
    let mut archive = Archive::new(decoder);

    if let Err(e) = archive.unpack(out_dir) {
        println!("cargo:warning=Failed to extract archive: {e}");
        return None;
    }

    // Clean up archive
    fs::remove_file(&archive_path).ok();

    // Verify extraction
    let lib_path = out_dir.join(lib_filename(target));
    if lib_path.exists() {
        println!("cargo:warning=Successfully downloaded native library");
        Some(out_dir.to_path_buf())
    } else {
        println!("cargo:warning=Archive extracted but library not found");
        None
    }
}

fn lib_filename(target: &str) -> String {
    if target.contains("windows") {
        format!("{LIB_NAME}.dll")
    } else if target.contains("darwin") || target.contains("apple") {
        format!("lib{LIB_NAME}.dylib")
    } else {
        format!("lib{LIB_NAME}.so")
    }
}

fn platform_dir(target: &str) -> &'static str {
    match target {
        t if t.contains("x86_64") && t.contains("linux") => "linux-x64",
        t if t.contains("aarch64") && t.contains("linux") => "linux-arm64",
        t if t.contains("x86_64") && (t.contains("darwin") || t.contains("apple")) => "darwin-x64",
        t if t.contains("aarch64") && (t.contains("darwin") || t.contains("apple")) => {
            "darwin-arm64"
        }
        t if t.contains("x86_64") && t.contains("windows") => "win32-x64",
        _ => "linux-x64", // fallback
    }
}