jq_src/
lib.rs

1//! This crate aims to encapsulate the logic required for building `libjq`
2//! from source (so that [jq-sys] doesn't have to know how to do this).
3//!
4//! The primary consumers of this crate are [jq-sys] (the generated bindings
5//! to `libjq`), and indirectly [json-query] (a high-level wrapper for running
6//! _jq programs_ over json strings).
7//!
8//! [jq-sys]: https://github.com/onelson/jq-sys
9//! [json-query]: https://github.com/onelson/json-jquery
10
11extern crate autotools;
12
13use std::env;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17/// Information about the locations of files generated by `build()`.
18///
19/// After the jq sources have been compiled, the fields in this struct
20/// represent where the various files ended up, and what sort of build was
21/// done (ie, static or dynamic).
22pub struct Artifacts {
23    include_dir: PathBuf,
24    lib_dir: PathBuf,
25}
26
27impl Artifacts {
28    /// Prints cargo instructions for linking to the bundled `libjq`.
29    pub fn print_cargo_metadata(&self) {
30        println!("cargo:include={}", self.include_dir.display());
31        println!("cargo:rustc-link-search=native={}", self.lib_dir.display());
32
33        for lib in &["onig", "jq"] {
34            println!("cargo:rustc-link-lib=static={}", lib);
35        }
36    }
37    pub fn include_dir(&self) -> &Path {
38        &self.include_dir
39    }
40    pub fn lib_dir(&self) -> &Path {
41        &self.lib_dir
42    }
43}
44
45/// Entry point for callers to run the build.
46pub fn build() -> Result<Artifacts, ()> {
47    let out_dir = env::var_os("OUT_DIR")
48        .map(PathBuf::from)
49        .expect("OUT_DIR not set");
50
51    // The `autotools` build has been shown to be somewhat unreliable.
52    // Intermittent failures have been shown to "disappear" when re-run, so
53    // we do this here by running the build in a loop.
54    // While it's not great to hard-code the limit here this deep in the build,
55    // we are returning a Result so if the caller wants to respond to the
56    // failure themselves they still can. This loop is just to paper over the
57    // common case.
58    //
59    // It's ugly, but having spent several hours trying to figure out how
60    // to solve this more correctly, I'm prepared to tolerate the
61    // spawn/loop.
62    //
63    // See for more info https://github.com/onelson/jq-src/issues/1
64    for i in 1..=3 {
65        match run_autotools(&out_dir) {
66            Err(_) if i < 3 => {
67                eprintln!("Build experienced some sort of failure. Retrying ({}).", i)
68            }
69            Ok(artifacts) => return Ok(artifacts),
70            _ => (),
71        }
72    }
73    Err(())
74}
75
76/// This function performs the build, wrapping it in a thread so the caller
77/// can observe the outcome without panicking.
78fn run_autotools(out_dir: &Path) -> Result<Artifacts, ()> {
79    // This is where we'll run the build from
80    let sources_dir = out_dir.join("sources");
81
82    // The location of the git submodule registered
83    // in with this crate's repo.
84    let modules_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("modules");
85
86    // basically just a recursive copy, with some cleanup.
87    prepare_sources(&modules_dir, &sources_dir);
88
89    // The `autotools` crate will panic when a command fails, so in order
90    // to add a retry behavior we need to put it on a separate thread.
91    let worker = {
92        let out = out_dir.to_path_buf();
93        let root = sources_dir.join("jq");
94
95        std::thread::spawn(move || {
96            autotools::Config::new(&root)
97                .reconf("-fi")
98                .out_dir(&out)
99                .disable("maintainer-mode", None)
100                .with("oniguruma", Some("builtin"))
101                .make_args(vec!["LDFLAGS=-all-static".into(), "CFLAGS=-fPIC".into()])
102                .build();
103        })
104    };
105
106    match worker.join() {
107        Ok(_) => Ok(Artifacts {
108            lib_dir: out_dir.join("lib"),
109            include_dir: out_dir.join("include"),
110        }),
111        _ => Err(()),
112    }
113}
114
115/// Recursive file copy
116fn cp_r(src: &Path, dst: &Path) {
117    for f in fs::read_dir(src).unwrap() {
118        let f = f.unwrap();
119        let path = f.path();
120        let name = path.file_name().unwrap();
121        let dst = dst.join(name);
122        if f.file_type().unwrap().is_dir() {
123            fs::create_dir_all(&dst).unwrap();
124            cp_r(&path, &dst);
125        } else {
126            let _ = fs::remove_file(&dst);
127            fs::copy(&path, &dst).unwrap();
128        }
129    }
130}
131
132/// Cleanup old sources (left from a previous build attempt) then copy from
133/// the git submodule into the location where the build will happen.
134fn prepare_sources(src: &Path, dst: &Path) {
135    if dst.exists() {
136        fs::remove_dir_all(dst).unwrap();
137    }
138    fs::create_dir_all(dst).unwrap();
139    cp_r(src, dst);
140}