cp2k-libxc-sys 0.3.1

Static Libxc build for cp2k-rs
//! Builds static Libxc (exchange–correlation functionals) with the Fortran 2003
//! interface (`libxcf03`) and exports its install prefix via `cargo:root`
//! (DEP_CP2K_LIBXC_ROOT). CP2K discovers it via `find_package(Libxc 7 CONFIG)`.
//!
//! Under the `cuda` feature the functional kernels are additionally compiled
//! for the GPU (`ENABLE_CUDA`; working GPU support is new in libxc 7.1.0). The
//! GPU build lands in its own `-cuda` stable dir (via `bu::stable_dir`'s
//! feature-derived suffix). libxc's CUDA kernels use relocatable device code
//! (functional TUs share `__device__` helpers), so a `nvcc --device-link` step
//! produces `libxc_dlink.o`, which the worker links (root build.rs). The GPU
//! workers already carry the CUDA runtime, so this adds no new class of
//! dynamic dependency; the CPU worker is unaffected. The `hip` feature does
//! not (yet) enable libxc's GPU path — the HIP worker gets a CPU libxc.
//! Pure C + Fortran on the host side — no C++, so no new dynamic deps there.
//!
//! 7.1.2 (was 7.1.0) picks up 7.1.1's fixes — notably a 2.4-4.9x performance
//! regression in HSE06/WPBEH (a piecewise helper was evaluating both branches
//! unconditionally) and several SIGFPE fixes in debug builds — plus 7.1.2's
//! own accuracy/performance work replacing `pow()` with direct arithmetic for
//! integer/rational exponents. CP2K's own `master` toolchain already pins
//! 7.1.2 (`support/v2026.2` still pins 7.0.0 as of this writing).

use cp2k_build_utils as bu;
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=CP2K_TARGET_ARCH");
    println!("cargo:rerun-if-env-changed=CP2K_CUDA_ARCH");
    println!("cargo:rerun-if-env-changed=CP2K_HIP_ARCH");

    let arch = bu::target_arch();
    // Version-suffixed stable dir (CI caches persist across version bumps and
    // the skip check below only tests for the installed archive); the GPU
    // feature suffix (-cuda/-hip) is appended by bu::stable_dir.
    let stable_dir = bu::stable_dir("libxc-v7.1");
    let src_dir = stable_dir.join("src");
    let build_dir = stable_dir.join("build");
    let install_dir = stable_dir.join("install");

    // 7.1.x is not on cp2k.org's mirror (the 2026.2 toolchain still pins
    // 7.0.0); pin the GitLab release source archive instead. The SHA-256
    // check fails loudly if GitLab ever regenerates the archive differently.
    let ver = "7.1.2";
    let sha = "3915fac94416e4c415534223ea492ad2663f928acf27e98662c861b094a6c306";

    // GPU kernels: CUDA only (libxc's HIP path via -fgpu-rdc device-linking is
    // not wired up yet — the HIP worker gets a CPU libxc, which is fine; its XC
    // is a small part of the runtime). The CUDA build uses relocatable device
    // code (RDC, required: functional TUs call shared __device__ helpers like
    // xc_expint_e1_impl across translation units). RDC objects reference
    // `__cudaRegisterLinkedBinary_*` symbols that a device-link step must
    // define; we run `nvcc -dlink` after the build to produce libxc_dlink.o and
    // the worker links that (see the root build.rs cuda block). The `.dlink`
    // recipe marker forces a rebuild of any GPU libxc cached before this fix
    // (earlier attempts left an RDC-off or link-broken libxc.a in the CI cache).
    //
    // The `.version` marker (unlike every other versioned `-sys` crate, libxc
    // never had one) catches a patch-level bump within the same "-v7.1"
    // stable-dir family — without it, a CI cache built for 7.1.0 would be
    // silently reused for 7.1.2 (skip check only tested "does libxc.a exist").
    // Written at `stable_dir` (install_dir's parent), matching every other
    // `-sys` crate: `inputs_fingerprint` and cp2k-sys's own deps-fingerprint
    // both look it up via `DEP_CP2K_LIBXC_ROOT.parent().join(".version")`, so
    // a marker at `install_dir` itself would be invisible to them — silently
    // reproducing the exact staleness class fixed for SIRIUS/COSMA.
    let cuda = cfg!(feature = "cuda");
    let recipe_ok = (!cuda || install_dir.join(".dlink").exists())
        && !bu::dep_marker_stale(&stable_dir, ".version", ver);
    if install_dir.join("lib").join("libxc.a").exists() && recipe_ok {
        println!(
            "cargo:warning=Libxc already installed at {}",
            install_dir.display()
        );
        bu::emit_root(&install_dir);
        return;
    }
    if install_dir.join("lib").join("libxc.a").exists() {
        println!(
            "cargo:warning=Cached Libxc is stale (version bump or device-link recipe changed) – rebuilding..."
        );
        let _ = std::fs::remove_dir_all(&install_dir);
        let _ = std::fs::remove_dir_all(&build_dir);
        let _ = std::fs::remove_dir_all(&src_dir);
    }

    if !src_dir.exists() {
        println!("cargo:warning=Downloading Libxc {ver}...");
        let tarball = stable_dir.join(format!("libxc-{ver}.tar.bz2"));
        bu::download(
            &format!("https://gitlab.com/libxc/libxc/-/archive/{ver}/libxc-{ver}.tar.bz2"),
            &tarball,
        );
        bu::verify_sha256(&tarball, sha);
        std::fs::create_dir_all(&src_dir).expect("Failed to create libxc src dir");
        bu::run(
            Command::new("tar").args([
                "-xjf",
                tarball.to_str().unwrap(),
                "--strip-components=1",
                "-C",
                src_dir.to_str().unwrap(),
            ]),
            "Libxc extract",
        );
    }

    std::fs::create_dir_all(&build_dir).expect("Failed to create libxc build dir");
    let num_jobs = bu::num_jobs();

    println!("cargo:warning=Configuring Libxc with CMake...");
    let mut args: Vec<String> = vec![
        "-S".into(),
        src_dir.to_string_lossy().into_owned(),
        "-B".into(),
        build_dir.to_string_lossy().into_owned(),
        "-DCMAKE_BUILD_TYPE=Release".into(),
        "-DBUILD_SHARED_LIBS=OFF".into(),
        "-DCMAKE_POSITION_INDEPENDENT_CODE=ON".into(),
        "-DBUILD_FPIC=ON".into(),
        "-DCMAKE_INSTALL_LIBDIR=lib".into(),
        "-DENABLE_FORTRAN=ON".into(),
        "-DENABLE_PYTHON=OFF".into(),
        "-DBUILD_TESTING=OFF".into(),
        "-DDISABLE_KXC=OFF".into(),
        format!("-DCMAKE_INSTALL_PREFIX={}", install_dir.display()),
        format!("-DCMAKE_C_FLAGS={}", bu::march_flag(&arch)),
        format!("-DCMAKE_Fortran_FLAGS={}", bu::march_flag(&arch)),
        format!("-DCMAKE_BUILD_PARALLEL_LEVEL={num_jobs}"),
    ];
    // CUDA GPU kernels (fat-binary arch list matching CP2K/DBCSR). HIP is left
    // CPU-only for now (see the note at the skip check above).
    let cuda_arch = std::env::var("CP2K_CUDA_ARCH").unwrap_or_else(|_| "80".into());
    if cuda {
        args.push("-DENABLE_CUDA=ON".into());
        args.push(format!("-DCMAKE_CUDA_ARCHITECTURES={cuda_arch}"));
    }
    bu::run(Command::new("cmake").args(&args), "Libxc configure");

    println!("cargo:warning=Building Libxc...");
    bu::run(
        Command::new("cmake").args(["--build", build_dir.to_str().unwrap(), "-j", &num_jobs]),
        "Libxc build",
    );
    bu::run(
        Command::new("cmake").args(["--install", build_dir.to_str().unwrap()]),
        "Libxc install",
    );

    if cuda {
        // Device-link the RDC objects in libxc.a into libxc_dlink.o: this
        // resolves the cross-TU __device__ calls and defines the
        // `__cudaRegisterLinkedBinary_*` symbols the host objects reference, so
        // the (non-CUDA-aware) rustc/cc final link of the worker succeeds. The
        // worker links libxc_dlink.o alongside libxc.a (root build.rs cuda
        // block). One `-gencode` per target architecture in the fat binary.
        let libxc_a = install_dir.join("lib").join("libxc.a");
        let dlink_o = install_dir.join("lib").join("libxc_dlink.o");
        let nvcc = std::env::var("CUDA_PATH")
            .map(|p| format!("{p}/bin/nvcc"))
            .unwrap_or_else(|_| "nvcc".into());
        let mut dlink = Command::new(&nvcc);
        dlink.arg("--device-link");
        for a in cuda_arch.split(';').filter(|s| !s.is_empty()) {
            dlink.arg(format!("-gencode=arch=compute_{a},code=sm_{a}"));
        }
        dlink
            .arg(&libxc_a)
            .arg("-o")
            .arg(&dlink_o)
            .arg("-Xcompiler")
            .arg("-fPIC");
        println!("cargo:warning=Device-linking libxc CUDA objects (nvcc --device-link)...");
        bu::run(&mut dlink, "Libxc CUDA device-link");
        std::fs::write(install_dir.join(".dlink"), "1\n").ok();
    }

    println!("cargo:warning=Libxc installed to {}", install_dir.display());
    bu::write_dep_marker(&stable_dir, ".version", ver);
    bu::emit_root(&install_dir);
}