1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! 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);
}