11use fs_extra:: dir:: CopyOptions ;
22use glob:: glob;
33use std:: env;
4- use std:: path:: PathBuf ;
4+ use std:: path:: { Path , PathBuf } ;
55use 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 ( "\n extern \" 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