1818const ZSTD_COMPRESSION_LEVEL : i32 = 19 ;
1919const DEFAULT_SDK_VERSION : & str = "27" ;
2020
21+ // SQLite version to build - 3.51.2 (latest as of Jan 2026)
22+ const SQLITE_VERSION : & str = "3510200" ;
23+ const SQLITE_YEAR : & str = "2026" ;
24+
2125#[ cfg( any( target_os = "macos" , target_os = "windows" ) ) ]
2226const PYTHON_EXECUTABLE : & str = "python.exe" ;
2327#[ cfg( not( any( target_os = "macos" , target_os = "windows" ) ) ) ]
@@ -290,7 +294,10 @@ fn maybe_make_cpython(repo_dir: &Path, wasi_sdk: &Path) -> Result<()> {
290294 . current_dir ( & cpython_native_dir)
291295 . arg ( format ! (
292296 "--prefix={}/install" ,
293- cpython_native_dir. to_str( ) . unwrap( )
297+ cpython_native_dir. to_str( ) . ok_or_else( || anyhow!(
298+ "non-UTF8 path: {}" ,
299+ cpython_native_dir. display( )
300+ ) ) ?
294301 ) ) ) ?;
295302
296303 run ( Command :: new ( "make" ) . current_dir ( cpython_native_dir) ) ?;
@@ -299,47 +306,53 @@ fn maybe_make_cpython(repo_dir: &Path, wasi_sdk: &Path) -> Result<()> {
299306 let lib_install_dir = cpython_wasi_dir. join ( "deps" ) ;
300307 build_zlib ( wasi_sdk, & lib_install_dir) ?;
301308
309+ build_sqlite ( wasi_sdk, & lib_install_dir) ?;
310+
302311 let config_guess =
303312 run ( Command :: new ( "../../config.guess" ) . current_dir ( & cpython_wasi_dir) ) ?;
304313
305314 let dir = cpython_wasi_dir
306315 . to_str ( )
307- . ok_or_else ( || anyhow ! ( "non-utf8 path: {}" , cpython_wasi_dir. display( ) ) ) ?;
316+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , cpython_wasi_dir. display( ) ) ) ?;
308317
318+ // Configure CPython with SQLite support
319+ // The CFLAGS and LDFLAGS now include paths to both zlib AND sqlite
309320 run ( Command :: new ( "../../Tools/wasm/wasi-env" )
310321 . env (
311322 "CONFIG_SITE" ,
312323 "../../Tools/wasm/wasi/config.site-wasm32-wasi" ,
313324 )
314325 . env (
315326 "CFLAGS" ,
316- format ! ( "--target=wasm32-wasip2 -fPIC -I{dir}/deps/include" , ) ,
327+ format ! ( "--target=wasm32-wasip2 -fPIC -I{dir}/deps/include" ) ,
317328 )
318329 . env ( "WASI_SDK_PATH" , wasi_sdk)
319330 . env (
320331 "LDFLAGS" ,
321- format ! ( "--target=wasm32-wasip2 -L{dir}/deps/lib" , ) ,
332+ format ! ( "--target=wasm32-wasip2 -L{dir}/deps/lib" ) ,
322333 )
323334 . current_dir ( & cpython_wasi_dir)
324335 . args ( [
325336 "../../configure" ,
326337 "-C" ,
327338 "--host=wasm32-unknown-wasip2" ,
328339 & format ! ( "--build={}" , String :: from_utf8( config_guess) ?) ,
329- & format ! (
330- "--with-build-python={}/../build/{PYTHON_EXECUTABLE}" ,
331- cpython_wasi_dir. to_str( ) . unwrap( )
332- ) ,
333- & format ! ( "--prefix={}/install" , cpython_wasi_dir. to_str( ) . unwrap( ) ) ,
340+ & format ! ( "--with-build-python={dir}/../build/{PYTHON_EXECUTABLE}" , ) ,
341+ & format ! ( "--prefix={dir}/install" ) ,
334342 "--disable-test-modules" ,
335343 "--enable-ipv6" ,
336344 ] ) ) ?;
337345
346+ // Write Modules/Setup.local to force-enable _sqlite3
347+ // This ensures the module is built even if configure doesn't auto-detect it
348+ write_setup_local ( & cpython_wasi_dir) ?;
349+
338350 run ( Command :: new ( "make" )
339351 . current_dir ( & cpython_wasi_dir)
340352 . args ( [ "build_all" , "install" ] ) ) ?;
341353 }
342354
355+ // Link libpython3.14.so - now includes libsqlite3.a
343356 run ( Command :: new ( wasi_sdk. join ( "bin/clang" ) )
344357 . arg ( "--target=wasm32-wasip2" )
345358 . arg ( "-shared" )
@@ -357,6 +370,7 @@ fn maybe_make_cpython(repo_dir: &Path, wasi_sdk: &Path) -> Result<()> {
357370 . arg ( cpython_wasi_dir. join ( "Modules/_decimal/libmpdec/libmpdec.a" ) )
358371 . arg ( cpython_wasi_dir. join ( "Modules/expat/libexpat.a" ) )
359372 . arg ( cpython_wasi_dir. join ( "deps/lib/libz.a" ) )
373+ . arg ( cpython_wasi_dir. join ( "deps/lib/libsqlite3.a" ) )
360374 . arg ( "-lwasi-emulated-signal" )
361375 . arg ( "-lwasi-emulated-getpid" )
362376 . arg ( "-lwasi-emulated-process-clocks" )
@@ -366,6 +380,45 @@ fn maybe_make_cpython(repo_dir: &Path, wasi_sdk: &Path) -> Result<()> {
366380 Ok ( ( ) )
367381}
368382
383+ /// Write Modules/Setup.local to enable _sqlite3 module
384+ ///
385+ /// CPython's configure may not auto-detect sqlite3 for WASI cross-compilation,
386+ /// so we explicitly enable it here.
387+ fn write_setup_local ( cpython_wasi_dir : & Path ) -> Result < ( ) > {
388+ let setup_local_path = cpython_wasi_dir. join ( "Modules/Setup.local" ) ;
389+ let deps_dir = cpython_wasi_dir. join ( "deps" ) ;
390+
391+ // The _sqlite3 module source files (relative to Modules/)
392+ // These are the files that make up the _sqlite3 extension in CPython 3.14
393+ // Note: blob.c is required - it defines pysqlite_close_all_blobs and pysqlite_blob_setup_types
394+ let include_dir = deps_dir. join ( "include" ) ;
395+ let lib_dir = deps_dir. join ( "lib" ) ;
396+ let setup_local_content = format ! (
397+ r#"# Auto-generated by build.rs for SQLite support
398+ # Enable _sqlite3 module with statically linked SQLite
399+
400+ _sqlite3 _sqlite/blob.c _sqlite/connection.c _sqlite/cursor.c _sqlite/microprotocols.c _sqlite/module.c _sqlite/prepare_protocol.c _sqlite/row.c _sqlite/statement.c _sqlite/util.c -I{include} -L{lib} -lsqlite3
401+ "# ,
402+ include = include_dir
403+ . to_str( )
404+ . ok_or_else( || anyhow!( "non-UTF8 path: {}" , include_dir. display( ) ) ) ?,
405+ lib = lib_dir
406+ . to_str( )
407+ . ok_or_else( || anyhow!( "non-UTF8 path: {}" , lib_dir. display( ) ) ) ?,
408+ ) ;
409+
410+ // Create the Modules directory if it doesn't exist
411+ fs:: create_dir_all ( cpython_wasi_dir. join ( "Modules" ) ) ?;
412+ fs:: write ( & setup_local_path, setup_local_content) ?;
413+
414+ println ! (
415+ "cargo:warning=Wrote Modules/Setup.local to enable _sqlite3: {}" ,
416+ setup_local_path. display( )
417+ ) ;
418+
419+ Ok ( ( ) )
420+ }
421+
369422fn run ( command : & mut Command ) -> Result < Vec < u8 > > {
370423 let command_string = iter:: once ( command. get_program ( ) )
371424 . chain ( command. get_args ( ) )
@@ -525,7 +578,7 @@ fn build_zlib(wasi_sdk: &Path, install_dir: &Path) -> Result<()> {
525578
526579 let prefix = install_dir
527580 . to_str ( )
528- . ok_or_else ( || anyhow ! ( "non-utf8 path: {}" , install_dir. display( ) ) ) ?;
581+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , install_dir. display( ) ) ) ?;
529582
530583 let mut configure = Command :: new ( "./configure" ) ;
531584 add_compile_envs ( wasi_sdk, & mut configure) ;
@@ -538,12 +591,12 @@ fn build_zlib(wasi_sdk: &Path, install_dir: &Path) -> Result<()> {
538591 let ar_dir = wasi_sdk. join ( "bin/ar" ) ;
539592 let ar_dir = ar_dir
540593 . to_str ( )
541- . ok_or_else ( || anyhow ! ( "non-utf8 path: {}" , ar_dir. display( ) ) ) ?;
594+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , ar_dir. display( ) ) ) ?;
542595
543596 let clang_dir = wasi_sdk. join ( "bin/clang" ) ;
544597 let clang_dir = clang_dir
545598 . to_str ( )
546- . ok_or_else ( || anyhow ! ( "non-utf8 path: {}" , clang_dir. display( ) ) ) ?;
599+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , clang_dir. display( ) ) ) ?;
547600
548601 let mut make = Command :: new ( "make" ) ;
549602 add_compile_envs ( wasi_sdk, & mut make) ;
@@ -557,3 +610,119 @@ fn build_zlib(wasi_sdk: &Path, install_dir: &Path) -> Result<()> {
557610
558611 Ok ( ( ) )
559612}
613+
614+ /// Build SQLite for WASI
615+ ///
616+ /// Downloads the SQLite amalgamation source and builds it as a static library
617+ /// for WASI. Key configuration:
618+ /// - SQLITE_OMIT_WAL: WAL requires mmap which isn't available in WASI preview1
619+ /// - SQLITE_OMIT_LOAD_EXTENSION: No dlopen in WASI
620+ /// - SQLITE_THREADSAFE=0: Single-threaded for WASM
621+ fn build_sqlite ( wasi_sdk : & Path , install_dir : & Path ) -> Result < ( ) > {
622+ let out_dir = PathBuf :: from ( env:: var ( "OUT_DIR" ) ?) ;
623+
624+ // Check if already built
625+ if install_dir. join ( "lib/libsqlite3.a" ) . exists ( ) {
626+ println ! ( "cargo:warning=SQLite already built, skipping" ) ;
627+ return Ok ( ( ) ) ;
628+ }
629+
630+ println ! ( "cargo:warning=Building SQLite {SQLITE_VERSION} for WASI..." ) ;
631+
632+ // Download SQLite amalgamation
633+ let url = format ! ( "https://sqlite.org/{SQLITE_YEAR}/sqlite-autoconf-{SQLITE_VERSION}.tar.gz" ) ;
634+ fetch_extract ( & url, & out_dir) ?;
635+
636+ let src_dir = out_dir. join ( format ! ( "sqlite-autoconf-{SQLITE_VERSION}" ) ) ;
637+
638+ // Ensure install directories exist
639+ fs:: create_dir_all ( install_dir. join ( "lib" ) ) ?;
640+ fs:: create_dir_all ( install_dir. join ( "include" ) ) ?;
641+
642+ let sysroot = wasi_sdk. join ( "share/wasi-sysroot" ) ;
643+ let sysroot_str = sysroot
644+ . to_str ( )
645+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , sysroot. display( ) ) ) ?;
646+ let install_dir_str = install_dir
647+ . to_str ( )
648+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , install_dir. display( ) ) ) ?;
649+ let ar_path = wasi_sdk. join ( "bin/ar" ) ;
650+ let ar_str = ar_path
651+ . to_str ( )
652+ . ok_or_else ( || anyhow ! ( "non-UTF8 path: {}" , ar_path. display( ) ) ) ?;
653+
654+ // SQLite-specific CFLAGS for WASI compatibility
655+ // Note: Don't set SQLITE_THREADSAFE here - let --disable-threadsafe handle it
656+ // to avoid macro redefinition warnings
657+ let sqlite_cflags = format ! (
658+ "--target=wasm32-wasi \
659+ --sysroot={sysroot_str} \
660+ -I{sysroot_str}/include/wasm32-wasip1 \
661+ -D_WASI_EMULATED_SIGNAL \
662+ -D_WASI_EMULATED_PROCESS_CLOCKS \
663+ -fPIC \
664+ -O2 \
665+ -DSQLITE_OMIT_WAL \
666+ -DSQLITE_OMIT_LOAD_EXTENSION \
667+ -DSQLITE_OMIT_LOCALTIME \
668+ -DSQLITE_OMIT_RANDOMNESS \
669+ -DSQLITE_OMIT_SHARED_CACHE",
670+ ) ;
671+
672+ // Configure SQLite
673+ let mut configure = Command :: new ( "./configure" ) ;
674+ configure
675+ . current_dir ( & src_dir)
676+ . env ( "AR" , wasi_sdk. join ( "bin/ar" ) )
677+ . env ( "CC" , wasi_sdk. join ( "bin/clang" ) )
678+ . env ( "RANLIB" , wasi_sdk. join ( "bin/ranlib" ) )
679+ . env ( "CFLAGS" , & sqlite_cflags)
680+ . env (
681+ "LDFLAGS" ,
682+ format ! ( "--target=wasm32-wasip2 --sysroot={sysroot_str} -L{sysroot_str}/lib" , ) ,
683+ )
684+ . arg ( "--host=wasm32-wasi" )
685+ . arg ( format ! ( "--prefix={install_dir_str}" ) )
686+ . arg ( "--disable-shared" )
687+ . arg ( "--enable-static" )
688+ . arg ( "--disable-readline" )
689+ . arg ( "--disable-threadsafe" )
690+ . arg ( "--disable-load-extension" ) ;
691+
692+ run ( & mut configure) ?;
693+
694+ // Build only the static library (not the shell, which fails to link on WASI)
695+ let mut make = Command :: new ( "make" ) ;
696+ make. current_dir ( & src_dir)
697+ . env ( "AR" , wasi_sdk. join ( "bin/ar" ) )
698+ . env ( "CC" , wasi_sdk. join ( "bin/clang" ) )
699+ . env ( "RANLIB" , wasi_sdk. join ( "bin/ranlib" ) )
700+ . env ( "CFLAGS" , & sqlite_cflags)
701+ . arg ( format ! ( "AR={ar_str}" ) )
702+ . arg ( "ARFLAGS=rcs" )
703+ . arg ( "libsqlite3.a" ) ; // Build only the static library
704+ run ( & mut make) ?;
705+
706+ // Manual install since we didn't build everything
707+ // Copy the library
708+ fs:: copy (
709+ src_dir. join ( "libsqlite3.a" ) ,
710+ install_dir. join ( "lib/libsqlite3.a" ) ,
711+ ) ?;
712+ // Copy the headers
713+ fs:: copy (
714+ src_dir. join ( "sqlite3.h" ) ,
715+ install_dir. join ( "include/sqlite3.h" ) ,
716+ ) ?;
717+ fs:: copy (
718+ src_dir. join ( "sqlite3ext.h" ) ,
719+ install_dir. join ( "include/sqlite3ext.h" ) ,
720+ ) ?;
721+
722+ println ! (
723+ "cargo:warning=SQLite built successfully: {}" ,
724+ install_dir. join( "lib/libsqlite3.a" ) . display( )
725+ ) ;
726+
727+ Ok ( ( ) )
728+ }
0 commit comments