spec-ai-cli 0.4.12

A framework for building AI agents with structured outputs, policy enforcement, and execution tracking
use std::env;
use std::error::Error;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    println!("cargo:rerun-if-env-changed=SPEC_AI_TIKA_CACHE_DIR");
    println!("cargo:rerun-if-env-changed=CARGO_INSTALL_ROOT");
    println!("cargo:rerun-if-env-changed=CARGO_HOME");
    println!("cargo:rerun-if-env-changed=HOME");

    if env::var("DOCS_RS").is_ok() {
        return;
    }

    match persist_extractous_libs() {
        Ok(Some(dir)) => {
            let dir_str = dir.display().to_string();
            println!("cargo:rustc-env=SPEC_AI_EXTRACTOUS_LIB_DIR={dir_str}");

            let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default();
            if target_family != "windows" {
                println!("cargo:rustc-link-arg=-Wl,-rpath,{dir_str}");
            }
        }
        Ok(None) => {}
        Err(err) => {
            println!("cargo:warning=Failed to persist extractous native libs: {err}");
        }
    }
}

fn persist_extractous_libs() -> Result<Option<PathBuf>, Box<dyn Error>> {
    let libs_dir = match locate_extractous_libs()? {
        Some(path) => path,
        None => return Ok(None),
    };

    let target = env::var("TARGET")?;
    let destination = determine_cache_dir(&target)?;

    copy_directory(&libs_dir, &destination)?;

    Ok(Some(destination))
}

fn locate_extractous_libs() -> Result<Option<PathBuf>, Box<dyn Error>> {
    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    let build_root = out_dir
        .parent()
        .and_then(|p| p.parent())
        .ok_or("unable to determine build directory for spec-ai-cli")?;

    let mut newest: Option<(SystemTime, PathBuf)> = None;

    for entry in fs::read_dir(build_root)? {
        let entry = match entry {
            Ok(value) => value,
            Err(_) => continue,
        };

        let name = match entry.file_name().into_string() {
            Ok(name) => name,
            Err(_) => continue,
        };

        if !name.starts_with("extractous-") {
            continue;
        }

        let candidate = entry.path().join("out").join("libs");
        if !candidate.exists() {
            continue;
        }

        let modified = fs::metadata(&candidate)
            .and_then(|meta| meta.modified())
            .unwrap_or(UNIX_EPOCH);

        match &mut newest {
            Some((current, path)) => {
                if modified > *current {
                    *current = modified;
                    *path = candidate;
                }
            }
            None => newest = Some((modified, candidate)),
        }
    }

    Ok(newest.map(|(_, path)| path))
}

fn determine_cache_dir(target: &str) -> Result<PathBuf, Box<dyn Error>> {
    let base = if let Ok(custom) = env::var("SPEC_AI_TIKA_CACHE_DIR") {
        PathBuf::from(custom)
    } else {
        default_cache_base().ok_or("SPEC_AI_TIKA_CACHE_DIR, CARGO_INSTALL_ROOT, CARGO_HOME, or HOME must be set")?
    };

    let version = env::var("CARGO_PKG_VERSION")?;

    Ok(base.join(target).join(version))
}

fn default_cache_base() -> Option<PathBuf> {
    if let Ok(root) = env::var("CARGO_INSTALL_ROOT") {
        return Some(PathBuf::from(root).join("lib").join("spec-ai").join("extractous"));
    }

    if let Ok(home) = env::var("CARGO_HOME") {
        return Some(PathBuf::from(home).join("lib").join("spec-ai").join("extractous"));
    }

    env::var("HOME")
        .map(|home| PathBuf::from(home).join(".spec-ai").join("extractous"))
        .ok()
}

fn copy_directory(src: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
    if dest.exists() {
        fs::remove_dir_all(dest)?;
    }

    fs::create_dir_all(dest)?;
    copy_dir_recursive(src, dest)?;
    Ok(())
}

fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let entry_path = entry.path();
        let dest_path = dest.join(entry.file_name());

        if file_type.is_dir() {
            fs::create_dir_all(&dest_path)?;
            copy_dir_recursive(&entry_path, &dest_path)?;
        } else if file_type.is_file() {
            fs::copy(&entry_path, &dest_path)?;
        }
    }

    Ok(())
}