Skip to content

Commit c63ff0c

Browse files
authored
chore(pgt_query): support system build (#576)
after reviewing the comments on our first attempt to add this project to homebrew (Homebrew/homebrew-core#218049), i realised that we can significantly improve the build in our homebrew recipe if we add `libpg_query` as a homebrew dependency (yes, it is available there as a formula). this PR adds a "dual build" to the build script of `pgt_query`. If `LIBPG_QUERY_PATH` is set, we link the library directly and skip the build. tested this locally. once this is merged, I will publish a new release and create the pr for homebrew.
1 parent 37ff15a commit c63ff0c

File tree

1 file changed

+201
-126
lines changed

1 file changed

+201
-126
lines changed

crates/pgt_query/build.rs

Lines changed: 201 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,96 @@
11
use fs_extra::dir::CopyOptions;
22
use glob::glob;
33
use std::env;
4-
use std::path::PathBuf;
4+
use std::path::{Path, PathBuf};
55
use std::process::Command;
66

7-
static LIBRARY_NAME: &str = "pg_query";
7+
static LIB_NAME: &str = "pg_query";
88

9-
fn main() -> Result<(), Box<dyn std::error::Error>> {
10-
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
11-
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
12-
let libpg_query_submodule = manifest_dir.join("vendor").join("libpg_query");
13-
14-
let src_dir = manifest_dir.join("src");
15-
let target = env::var("TARGET").unwrap();
16-
let is_emscripten = target.contains("emscripten");
9+
struct Layout {
10+
include_dir: PathBuf,
11+
lib_dir: Option<PathBuf>, // Some => system/dynamic; None => vendored/static
12+
header: PathBuf,
13+
proto: Option<PathBuf>,
14+
c_src_roots: Vec<PathBuf>,
15+
extra_includes: Vec<PathBuf>,
16+
build_root: PathBuf, // base where vendor/protobuf live
17+
}
1718

18-
println!("cargo:rustc-link-search=native={}", out_dir.display());
19-
println!("cargo:rustc-link-lib=static={LIBRARY_NAME}");
19+
fn system_layout(prefix: &Path) -> Result<Layout, String> {
20+
let include = prefix.join("include");
21+
let lib = prefix.join("lib");
22+
let header = include.join(format!("{LIB_NAME}.h"));
23+
if !header.exists() {
24+
return Err(format!(
25+
"LIBPG_QUERY_PATH set, but header not found: {}",
26+
header.display()
27+
));
28+
}
29+
let sys_proto = prefix.join("protobuf").join(format!("{LIB_NAME}.proto"));
30+
Ok(Layout {
31+
include_dir: include,
32+
lib_dir: Some(lib),
33+
header,
34+
proto: sys_proto.exists().then_some(sys_proto),
35+
c_src_roots: vec![],
36+
extra_includes: vec![],
37+
build_root: prefix.to_path_buf(),
38+
})
39+
}
2040

21-
// Check if submodule exists
22-
if !libpg_query_submodule.join(".git").exists() && !libpg_query_submodule.join("src").exists() {
41+
fn vendored_layout(vendor_root: &Path, out_dir: &Path) -> Result<Layout, String> {
42+
// Ensure submodule content exists
43+
if !vendor_root.join("src").exists() {
2344
return Err(
24-
"libpg_query submodule not found. Please run: git submodule update --init --recursive"
25-
.into(),
45+
"libpg_query submodule not found. Run: git submodule update --init --recursive".into(),
2646
);
2747
}
2848

29-
// Tell cargo to rerun if the submodule changes
30-
println!(
31-
"cargo:rerun-if-changed={}",
32-
libpg_query_submodule.join("src").display()
33-
);
34-
35-
// copy necessary files to out_dir for compilation
36-
let out_header_path = out_dir.join(LIBRARY_NAME).with_extension("h");
37-
let out_protobuf_path = out_dir.join("protobuf");
38-
39-
let source_paths = vec![
40-
libpg_query_submodule.join(LIBRARY_NAME).with_extension("h"),
41-
libpg_query_submodule.join("postgres_deparse.h"),
42-
libpg_query_submodule.join("Makefile"),
43-
libpg_query_submodule.join("src"),
44-
libpg_query_submodule.join("protobuf"),
45-
libpg_query_submodule.join("vendor"),
46-
];
47-
48-
let copy_options = CopyOptions {
49+
// Copy vendored tree into OUT_DIR
50+
let copy_opts = CopyOptions {
4951
overwrite: true,
5052
..CopyOptions::default()
5153
};
54+
let items = vec![
55+
vendor_root.join(format!("{LIB_NAME}.h")),
56+
vendor_root.join("postgres_deparse.h"),
57+
vendor_root.join("Makefile"),
58+
vendor_root.join("src"),
59+
vendor_root.join("protobuf"),
60+
vendor_root.join("vendor"),
61+
];
62+
fs_extra::copy_items(&items, out_dir, &copy_opts).map_err(|e| e.to_string())?;
5263

53-
fs_extra::copy_items(&source_paths, &out_dir, &copy_options)?;
54-
55-
// compile the c library.
56-
let mut build = cc::Build::new();
64+
let root = out_dir.to_path_buf();
65+
let out_header = root.join(format!("{LIB_NAME}.h"));
66+
let out_proto = root.join("protobuf").join(format!("{LIB_NAME}.proto"));
5767

58-
// configure for emscripten if needed
59-
if is_emscripten {
60-
// use emcc as the compiler instead of gcc/clang
61-
build.compiler("emcc");
62-
// use emar as the archiver instead of ar
63-
build.archiver("emar");
64-
// note: we don't add wasm-specific flags here as this creates a static library
65-
// the final linking flags should be added when building the final wasm module
66-
}
68+
let extra_includes = vec![
69+
root.join("."),
70+
root.join("vendor"),
71+
root.join("src/postgres/include"),
72+
root.join("src/include"),
73+
];
6774

68-
build
69-
.files(
70-
glob(out_dir.join("src/*.c").to_str().unwrap())
71-
.unwrap()
72-
.map(|p| p.unwrap()),
73-
)
74-
.files(
75-
glob(out_dir.join("src/postgres/*.c").to_str().unwrap())
76-
.unwrap()
77-
.map(|p| p.unwrap()),
78-
)
79-
.file(out_dir.join("vendor/protobuf-c/protobuf-c.c"))
80-
.file(out_dir.join("vendor/xxhash/xxhash.c"))
81-
.file(out_dir.join("protobuf/pg_query.pb-c.c"))
82-
.include(out_dir.join("."))
83-
.include(out_dir.join("./vendor"))
84-
.include(out_dir.join("./src/postgres/include"))
85-
.include(out_dir.join("./src/include"))
86-
.warnings(false); // avoid unnecessary warnings, as they are already considered as part of libpg_query development
87-
if env::var("PROFILE").unwrap() == "debug" || env::var("DEBUG").unwrap() == "1" {
88-
build.define("USE_ASSERT_CHECKING", None);
89-
}
90-
if target.contains("windows") && !is_emscripten {
91-
build.include(out_dir.join("./src/postgres/include/port/win32"));
92-
if target.contains("msvc") {
93-
build.include(out_dir.join("./src/postgres/include/port/win32_msvc"));
94-
}
95-
}
96-
build.compile(LIBRARY_NAME);
75+
Ok(Layout {
76+
include_dir: root.clone(),
77+
lib_dir: None,
78+
header: out_header,
79+
proto: out_proto.exists().then_some(out_proto),
80+
c_src_roots: vec![root.join("src"), root.join("src/postgres")],
81+
extra_includes,
82+
build_root: root,
83+
})
84+
}
9785

98-
// Generate bindings for Rust
99-
let mut bindgen_builder = bindgen::Builder::default()
100-
.header(out_header_path.to_str().ok_or("Invalid header path")?)
86+
fn run_bindgen(
87+
header: &Path,
88+
include_dirs: &[PathBuf],
89+
is_emscripten: bool,
90+
out_bindings: &Path,
91+
) -> Result<(), String> {
92+
let mut b = bindgen::Builder::default()
93+
.header(header.to_str().unwrap())
10194
// Allowlist only the functions we need
10295
.allowlist_function("pg_query_parse_protobuf")
10396
.allowlist_function("pg_query_scan")
@@ -128,47 +121,149 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
128121
.allowlist_type("size_t")
129122
.allowlist_var("PG_VERSION_NUM");
130123

131-
// Configure bindgen for Emscripten target
124+
for inc in include_dirs {
125+
b = b.clang_arg(format!("-I{}", inc.display()));
126+
}
127+
132128
if is_emscripten {
133-
// Tell bindgen to generate bindings for the wasm32 target
134-
bindgen_builder = bindgen_builder.clang_arg("--target=wasm32-unknown-emscripten");
129+
b = b.clang_arg("--target=wasm32-unknown-emscripten");
135130

136131
// Add emscripten sysroot includes
137-
// First try to use EMSDK environment variable (set in CI and when sourcing emsdk_env.sh)
138132
if let Ok(emsdk) = env::var("EMSDK") {
139-
bindgen_builder = bindgen_builder.clang_arg(format!(
133+
b = b.clang_arg(format!(
140134
"-I{emsdk}/upstream/emscripten/cache/sysroot/include"
141135
));
142136
} else {
143-
// Fallback to the default path if EMSDK is not set
144-
bindgen_builder =
145-
bindgen_builder.clang_arg("-I/emsdk/upstream/emscripten/cache/sysroot/include");
137+
b = b.clang_arg("-I/emsdk/upstream/emscripten/cache/sysroot/include");
146138
}
147139

148-
// Ensure we have the basic C standard library headers
149-
bindgen_builder = bindgen_builder.clang_arg("-D__EMSCRIPTEN__");
140+
b = b.clang_arg("-D__EMSCRIPTEN__");
150141

151-
// Use environment variable if set (from our justfile)
152-
if let Ok(extra_args) = env::var("BINDGEN_EXTRA_CLANG_ARGS") {
153-
for arg in extra_args.split_whitespace() {
154-
bindgen_builder = bindgen_builder.clang_arg(arg);
142+
if let Ok(extra) = env::var("BINDGEN_EXTRA_CLANG_ARGS") {
143+
for arg in extra.split_whitespace() {
144+
b = b.clang_arg(arg);
155145
}
156146
}
157147
}
158148

159-
let bindings = bindgen_builder
160-
.generate()
161-
.map_err(|_| "Unable to generate bindings")?;
149+
b.generate()
150+
.map_err(|_| "bindgen failed".to_string())?
151+
.write_to_file(out_bindings)
152+
.map_err(|e| e.to_string())
153+
}
154+
155+
fn maybe_generate_prost(proto_candidates: &[PathBuf], out_dir_src: &Path, out_dir_real: &Path) {
156+
let protoc_ok = Command::new("protoc")
157+
.arg("--version")
158+
.status()
159+
.ok()
160+
.map(|s| s.success())
161+
.unwrap_or(false);
162+
if !protoc_ok {
163+
println!("skipping protobuf generation (no protoc)");
164+
return;
165+
}
166+
let proto = proto_candidates.iter().find(|p| p.exists());
167+
if let Some(p) = proto {
168+
println!("generating protobuf from {}", p.display());
169+
unsafe {
170+
env::set_var("OUT_DIR", out_dir_src);
171+
}
172+
let inc = p.parent().unwrap();
173+
prost_build::compile_protos(&[p], &[inc]).expect("prost_build failed");
174+
std::fs::rename(
175+
out_dir_src.join("pg_query.rs"),
176+
out_dir_src.join("protobuf.rs"),
177+
)
178+
.ok();
179+
unsafe {
180+
env::set_var("OUT_DIR", out_dir_real);
181+
}
182+
} else {
183+
println!("skipping protobuf generation (no .proto found)");
184+
}
185+
}
186+
187+
fn compile_c_if_needed(layout: &Layout, is_emscripten: bool, target: &str) {
188+
if layout.lib_dir.is_some() {
189+
return;
190+
} // System lib, nothing to compile.
191+
192+
let mut cc = cc::Build::new();
193+
if is_emscripten {
194+
cc.compiler("emcc").archiver("emar");
195+
}
196+
197+
for root in &layout.c_src_roots {
198+
let pattern = root.join("*.c");
199+
for p in glob(pattern.to_str().unwrap()).unwrap().flatten() {
200+
cc.file(p);
201+
}
202+
}
203+
204+
// Add vendor files from copied tree
205+
cc.file(layout.build_root.join("vendor/protobuf-c/protobuf-c.c"));
206+
cc.file(layout.build_root.join("vendor/xxhash/xxhash.c"));
207+
cc.file(layout.build_root.join("protobuf/pg_query.pb-c.c"));
208+
209+
for inc in &layout.extra_includes {
210+
cc.include(inc);
211+
}
212+
cc.warnings(false);
213+
214+
let is_debug = env::var("PROFILE").ok().as_deref() == Some("debug")
215+
|| env::var("DEBUG").ok().as_deref() == Some("1");
216+
if is_debug {
217+
cc.define("USE_ASSERT_CHECKING", None);
218+
}
219+
if target.contains("windows") && !is_emscripten {
220+
cc.include(layout.include_dir.join("src/postgres/include/port/win32"));
221+
if target.contains("msvc") {
222+
cc.include(
223+
layout
224+
.include_dir
225+
.join("src/postgres/include/port/win32_msvc"),
226+
);
227+
}
228+
}
229+
230+
println!("cargo:rustc-link-lib=static={LIB_NAME}");
231+
cc.compile(LIB_NAME);
232+
}
233+
234+
fn main() -> Result<(), Box<dyn std::error::Error>> {
235+
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
236+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
237+
let src_dir = manifest_dir.join("src");
238+
let target = env::var("TARGET").unwrap();
239+
let is_emscripten = target.contains("emscripten");
240+
241+
println!("cargo:rustc-link-search=native={}", out_dir.display());
242+
243+
let layout = if let Ok(p) = env::var("LIBPG_QUERY_PATH") {
244+
println!("using system libpg_query at {p}");
245+
system_layout(Path::new(&p))?
246+
} else {
247+
println!("using vendored libpg_query (submodule)");
248+
let vendor_root = manifest_dir.join("vendor").join("libpg_query");
249+
vendored_layout(&vendor_root, &out_dir)?
250+
};
251+
252+
if let Some(lib_dir) = &layout.lib_dir {
253+
println!("cargo:rustc-link-search=native={}", lib_dir.display());
254+
println!("cargo:rustc-link-lib={LIB_NAME}");
255+
}
256+
257+
compile_c_if_needed(&layout, is_emscripten, &target);
162258

259+
let mut include_dirs = vec![layout.include_dir.clone()];
260+
include_dirs.extend(layout.extra_includes.clone());
163261
let bindings_path = out_dir.join("bindings.rs");
164-
bindings.write_to_file(&bindings_path)?;
262+
run_bindgen(&layout.header, &include_dirs, is_emscripten, &bindings_path)?;
165263

166-
// For WASM/emscripten builds, manually add the function declarations
167-
// since bindgen sometimes misses them due to preprocessor conditions
264+
// Emscripten-specific post-processing
168265
if is_emscripten {
169266
let mut bindings_content = std::fs::read_to_string(&bindings_path)?;
170-
171-
// Check if we need to add the extern "C" block
172267
if !bindings_content.contains("extern \"C\"") {
173268
bindings_content.push_str("\nextern \"C\" {\n");
174269
bindings_content.push_str(" pub fn pg_query_scan(input: *const ::std::os::raw::c_char) -> PgQueryScanResult;\n");
@@ -195,33 +290,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
195290
bindings_content
196291
.push_str(" pub fn pg_query_free_split_result(result: PgQuerySplitResult);\n");
197292
bindings_content.push_str("}\n");
198-
199293
std::fs::write(&bindings_path, bindings_content)?;
200294
}
201295
}
202296

203-
let protoc_exists = Command::new("protoc").arg("--version").status().is_ok();
204-
if protoc_exists {
205-
println!("generating protobuf bindings");
206-
// HACK: Set OUT_DIR to src/ so that the generated protobuf file is copied to src/protobuf.rs
207-
unsafe {
208-
env::set_var("OUT_DIR", &src_dir);
209-
}
210-
211-
prost_build::compile_protos(
212-
&[&out_protobuf_path.join(LIBRARY_NAME).with_extension("proto")],
213-
&[&out_protobuf_path],
214-
)?;
215-
216-
std::fs::rename(src_dir.join("pg_query.rs"), src_dir.join("protobuf.rs"))?;
217-
218-
// Reset OUT_DIR to the original value
219-
unsafe {
220-
env::set_var("OUT_DIR", &out_dir);
221-
}
222-
} else {
223-
println!("skipping protobuf generation");
224-
}
297+
// Protobuf generation (optional, uses pre-generated file as fallback)
298+
let candidates = layout.proto.into_iter().collect::<Vec<_>>();
299+
maybe_generate_prost(&candidates, &src_dir, &out_dir);
225300

226301
Ok(())
227302
}

0 commit comments

Comments
 (0)