jq-src 0.4.1

API for compiling and link libjq from source.
Documentation
//! This crate aims to encapsulate the logic required for building `libjq`
//! from source (so that [jq-sys] doesn't have to know how to do this).
//!
//! The primary consumers of this crate are [jq-sys] (the generated bindings
//! to `libjq`), and indirectly [json-query] (a high-level wrapper for running
//! _jq programs_ over json strings).
//!
//! [jq-sys]: https://github.com/onelson/jq-sys
//! [json-query]: https://github.com/onelson/json-jquery

extern crate autotools;

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

/// Information about the locations of files generated by `build()`.
///
/// After the jq sources have been compiled, the fields in this struct
/// represent where the various files ended up, and what sort of build was
/// done (ie, static or dynamic).
pub struct Artifacts {
    include_dir: PathBuf,
    lib_dir: PathBuf,
}

impl Artifacts {
    /// Prints cargo instructions for linking to the bundled `libjq`.
    pub fn print_cargo_metadata(&self) {
        println!("cargo:include={}", self.include_dir.display());
        println!("cargo:rustc-link-search=native={}", self.lib_dir.display());

        for lib in &["onig", "jq"] {
            println!("cargo:rustc-link-lib=static={}", lib);
        }
    }
    pub fn include_dir(&self) -> &Path {
        &self.include_dir
    }
    pub fn lib_dir(&self) -> &Path {
        &self.lib_dir
    }
}

/// Entry point for callers to run the build.
pub fn build() -> Result<Artifacts, ()> {
    let out_dir = env::var_os("OUT_DIR")
        .map(PathBuf::from)
        .expect("OUT_DIR not set");

    // The `autotools` build has been shown to be somewhat unreliable.
    // Intermittent failures have been shown to "disappear" when re-run, so
    // we do this here by running the build in a loop.
    // While it's not great to hard-code the limit here this deep in the build,
    // we are returning a Result so if the caller wants to respond to the
    // failure themselves they still can. This loop is just to paper over the
    // common case.
    //
    // It's ugly, but having spent several hours trying to figure out how
    // to solve this more correctly, I'm prepared to tolerate the
    // spawn/loop.
    //
    // See for more info https://github.com/onelson/jq-src/issues/1
    for i in 1..=3 {
        match run_autotools(&out_dir) {
            Err(_) if i < 3 => {
                eprintln!("Build experienced some sort of failure. Retrying ({}).", i)
            }
            Ok(artifacts) => return Ok(artifacts),
            _ => (),
        }
    }
    Err(())
}

/// This function performs the build, wrapping it in a thread so the caller
/// can observe the outcome without panicking.
fn run_autotools(out_dir: &Path) -> Result<Artifacts, ()> {
    // This is where we'll run the build from
    let sources_dir = out_dir.join("sources");

    // The location of the git submodule registered
    // in with this crate's repo.
    let modules_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("modules");

    // basically just a recursive copy, with some cleanup.
    prepare_sources(&modules_dir, &sources_dir);

    // The `autotools` crate will panic when a command fails, so in order
    // to add a retry behavior we need to put it on a separate thread.
    let worker = {
        let out = out_dir.to_path_buf();
        let root = sources_dir.join("jq");

        std::thread::spawn(move || {
            autotools::Config::new(&root)
                .reconf("-fi")
                .out_dir(&out)
                .disable("maintainer-mode", None)
                .with("oniguruma", Some("builtin"))
                .make_args(vec!["LDFLAGS=-all-static".into(), "CFLAGS=-fPIC".into()])
                .build();
        })
    };

    match worker.join() {
        Ok(_) => Ok(Artifacts {
            lib_dir: out_dir.join("lib"),
            include_dir: out_dir.join("include"),
        }),
        _ => Err(()),
    }
}

/// Recursive file copy
fn cp_r(src: &Path, dst: &Path) {
    for f in fs::read_dir(src).unwrap() {
        let f = f.unwrap();
        let path = f.path();
        let name = path.file_name().unwrap();
        let dst = dst.join(name);
        if f.file_type().unwrap().is_dir() {
            fs::create_dir_all(&dst).unwrap();
            cp_r(&path, &dst);
        } else {
            let _ = fs::remove_file(&dst);
            fs::copy(&path, &dst).unwrap();
        }
    }
}

/// Cleanup old sources (left from a previous build attempt) then copy from
/// the git submodule into the location where the build will happen.
fn prepare_sources(src: &Path, dst: &Path) {
    if dst.exists() {
        fs::remove_dir_all(dst).unwrap();
    }
    fs::create_dir_all(dst).unwrap();
    cp_r(src, dst);
}