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}