spiral-sdk 0.1.12

Rust SDK for Spiral resource and table APIs
Documentation
//! Link the prepared libspiral archive for this release and target.
//!
//! The archive is never built or downloaded here: `scripts/setup-native-runtime.py`, run from
//! this release's checkout, verifies and caches it before compilation, or
//! `SPIRAL_NATIVE_DIR` names a prepared directory.

use std::path::{Path, PathBuf};
use std::{env, fs};

fn main() {
    if env::var_os("CARGO_FEATURE_EMBEDDED").is_none() {
        return;
    }
    println!("cargo::rerun-if-env-changed=SPIRAL_NATIVE_DIR");
    println!("cargo::rerun-if-env-changed=SPIRAL_NATIVE_CACHE_DIR");
    println!("cargo::rerun-if-env-changed=XDG_CACHE_HOME");
    // The private runtime, linked from source, exports the ABI itself. Documentation builds
    // never link.
    if env::var_os("CARGO_FEATURE_SOURCE_RUNTIME").is_some() || env::var_os("DOCS_RS").is_some() {
        return;
    }
    let target = env::var("TARGET").expect("Cargo target");
    let version = env::var("CARGO_PKG_VERSION").expect("package version");
    let directory = match env::var_os("SPIRAL_NATIVE_DIR") {
        Some(directory) => PathBuf::from(directory),
        None => cache_directory().join(&version).join(&target),
    };
    match link_requirements(&directory) {
        Ok(requirements) => {
            println!("cargo::rerun-if-changed={}", directory.display());
            println!("cargo::rustc-link-search=native={}", directory.display());
            println!("cargo::rustc-link-lib=static=spiral");
            for requirement in requirements {
                println!("{requirement}");
            }
        }
        Err(problem) => println!(
            "cargo::error={problem} for {target} at {}; {}",
            directory.display(),
            setup_step()
        ),
    }
}

/// The system libraries a prepared archive needs, once it is known to match this ABI.
fn link_requirements(directory: &Path) -> Result<Vec<String>, String> {
    if !directory.join("libspiral.a").is_file() {
        return Err("no prepared libspiral archive".into());
    }
    // Lockstep releases share the generated header; a different one is a different ABI.
    let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest directory"));
    let header = fs::read(directory.join("scandal.h")).unwrap_or_default();
    if header != fs::read(manifest.join("include/scandal.h")).expect("published header") {
        return Err("the libspiral archive was built for a different ABI".into());
    }
    let requirements = fs::read_to_string(directory.join("link.txt"))
        .map_err(|_| "the libspiral archive has no link.txt")?;
    requirements
        .lines()
        .filter(|line| !line.is_empty())
        .map(|requirement| {
            // The archive names only the system libraries it needs, never other directives.
            if requirement.starts_with("cargo::rustc-link-lib=") {
                Ok(requirement.to_owned())
            } else {
                Err(format!("unexpected link requirement {requirement:?}"))
            }
        })
        .collect()
}

/// How to prepare the archive using the release checkout or packaged SDK script.
fn setup_step() -> String {
    let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest directory"));
    let packaged_script = manifest.join("scripts/setup-native-runtime.py");
    let script = if packaged_script.is_file() {
        packaged_script
    } else {
        manifest.join("../../../scripts/setup-native-runtime.py")
    };
    let script = fs::canonicalize(&script).unwrap_or(script);
    format!(
        "run `python3 {}` to download this release's verified archive (or its \
         `setup-native-runtime` action at this release's tag), or set SPIRAL_NATIVE_DIR \
         to a prepared archive directory",
        script.display()
    )
}

/// Where the setup script caches prepared archives, by release and target.
fn cache_directory() -> PathBuf {
    if let Some(directory) = env::var_os("SPIRAL_NATIVE_CACHE_DIR") {
        return PathBuf::from(directory);
    }
    let base = env::var_os("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))
        .expect("HOME or XDG_CACHE_HOME for the libspiral archive cache");
    base.join("spiral").join("native")
}