Skip to content

Commit 79e98c3

Browse files
sd2kclaude
andauthored
feat: add SQLite3 support to CPython WASI build (#195)
* Add SQLite3 support to CPython WASI build Downloads and builds SQLite 3.51.2 as a static library, then links it into libpython3.14.so. The _sqlite3 module is enabled via Modules/Setup.local. WASI-specific SQLite configuration: - SQLITE_OMIT_WAL (no mmap in WASI preview1) - SQLITE_OMIT_LOAD_EXTENSION (no dlopen) - SQLITE_THREADSAFE=0 (single-threaded WASM) Adds ~500KB to libpython3.14.so.zst (7.3MB → 7.8MB). * fix: use Path::to_str instead of display for compiler paths Path::display() does a lossy conversion which can cause subtle, hard-to-diagnose failures when passing paths to compiler commands. Use to_str() with explicit error handling instead, as suggested in PR review. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Use Path::to_str throughout build.rs --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4f8e446 commit 79e98c3

File tree

1 file changed

+181
-12
lines changed

1 file changed

+181
-12
lines changed

build.rs

Lines changed: 181 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ use {
1818
const ZSTD_COMPRESSION_LEVEL: i32 = 19;
1919
const 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"))]
2226
const 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+
369422
fn 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

Comments
 (0)