pglite_oxide/
lib.rs

1use anyhow::{anyhow, Context, Result};
2use directories::ProjectDirs;
3use std::{
4    fs,
5    io::{Cursor, Read},
6    path::{Path, PathBuf},
7};
8use tar::Archive;
9use tracing::{debug, info, warn};
10use xz2::read::XzDecoder;
11
12use flate2::read::GzDecoder;
13
14pub mod interactive;
15
16use cap_std::ambient_authority;
17use cap_std::fs::Dir;
18use wasmtime::{Engine, Linker, Module, Store};
19use wasmtime_wasi::preview1::add_to_linker_sync;
20use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder, WasiP1Ctx};
21
22/// Initialize tracing with verbose logging
23pub fn init_tracing() {
24    use tracing_subscriber::EnvFilter;
25
26    tracing_subscriber::fmt()
27        .with_env_filter(EnvFilter::new("pglite_oxide=trace,info"))
28        .init();
29}
30
31const EMBEDDED_TAR_XZ: &[u8] = include_bytes!("../assets/pglite-wasi.tar.xz");
32
33#[cfg(unix)]
34fn ensure_shim(src: &Path, dst: &Path) -> Result<()> {
35    use std::os::unix::fs::symlink;
36    if !dst.exists() {
37        if let Err(e) = symlink(src, dst) {
38            let _ = std::fs::copy(src, dst).with_context(|| {
39                format!(
40                    "copy {} -> {} (fallback after symlink error: {e})",
41                    src.display(),
42                    dst.display()
43                )
44            })?;
45        }
46    }
47    Ok(())
48}
49
50#[cfg(not(unix))]
51fn ensure_shim(src: &Path, dst: &Path) -> Result<()> {
52    if !dst.exists() {
53        std::fs::copy(src, dst)
54            .with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?;
55    }
56    Ok(())
57}
58
59pub(crate) fn seed_urandom_once(dev_host: &Path) -> Result<()> {
60    fs::create_dir_all(dev_host)?;
61    let urandom_path = dev_host.join("urandom");
62    if !urandom_path.exists() {
63        let mut buf = [0u8; 128];
64        getrandom::getrandom(&mut buf)?; // real entropy
65        fs::write(&urandom_path, buf)?; // create once
66    }
67    Ok(())
68}
69
70pub(crate) fn create_engine() -> Result<Engine> {
71    let mut cfg = wasmtime::Config::new();
72    cfg.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
73    Engine::new(&cfg)
74}
75
76pub(crate) fn prepare_guest_dirs(paths: &PglitePaths) -> Result<()> {
77    let dev_host = paths.pgroot.join("dev");
78    seed_urandom_once(&dev_host)?;
79    fs::create_dir_all(&paths.pgdata)?;
80    Ok(())
81}
82
83pub(crate) fn standard_wasi_builder(paths: &PglitePaths) -> Result<WasiCtxBuilder> {
84    prepare_guest_dirs(paths)?;
85
86    let pgroot_dir = Dir::open_ambient_dir(&paths.pgroot, ambient_authority())?;
87    let pgdata_dir = Dir::open_ambient_dir(&paths.pgdata, ambient_authority())?;
88    let dev_dir_path = paths.pgroot.join("dev");
89    let dev_dir = Dir::open_ambient_dir(&dev_dir_path, ambient_authority())?;
90
91    let mut builder = WasiCtxBuilder::new();
92    builder
93        .inherit_stdin()
94        .inherit_stdout()
95        .inherit_stderr()
96        .preopened_dir(pgroot_dir, DirPerms::all(), FilePerms::all(), "/tmp")
97        .preopened_dir(
98            pgdata_dir,
99            DirPerms::all(),
100            FilePerms::all(),
101            "/tmp/pglite/base",
102        )
103        .preopened_dir(dev_dir, DirPerms::all(), FilePerms::all(), "/dev")
104        .env("ENVIRONMENT", "wasm32_wasi_preview1")
105        .env("PREFIX", "/tmp/pglite")
106        .env("PGDATA", "/tmp/pglite/base")
107        .env("PGSYSCONFDIR", "/tmp/pglite")
108        .env("PGUSER", "postgres")
109        .env("PGDATABASE", "template1")
110        .env("MODE", "REACT")
111        .env("REPL", "N")
112        .env("TZ", "UTC")
113        .env("PGTZ", "UTC")
114        .env("PATH", "/tmp/pglite/bin");
115    Ok(builder)
116}
117
118fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
119    fs::create_dir_all(dst)?;
120    for entry in fs::read_dir(src)? {
121        let entry = entry?;
122        let src_path = entry.path();
123        let dst_path = dst.join(entry.file_name());
124        if src_path.is_dir() {
125            copy_dir_all(&src_path, &dst_path)?;
126        } else {
127            fs::copy(&src_path, &dst_path)?;
128        }
129    }
130    Ok(())
131}
132
133fn assets_dir() -> PathBuf {
134    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets")
135}
136
137fn install_optional_pg_dump(paths: &PglitePaths) -> Result<()> {
138    let src = assets_dir().join("bin/pg_dump.wasm");
139    if !src.exists() {
140        return Ok(());
141    }
142
143    let dest_dir = paths.pgroot.join("pglite/bin");
144    fs::create_dir_all(&dest_dir)?;
145
146    let wasm_dest = dest_dir.join("pg_dump.wasm");
147    fs::copy(&src, &wasm_dest)
148        .with_context(|| format!("copy {} -> {}", src.display(), wasm_dest.display()))?;
149
150    let plain_dest = dest_dir.join("pg_dump");
151    if !plain_dest.exists() {
152        fs::copy(&wasm_dest, &plain_dest).ok();
153    }
154
155    Ok(())
156}
157
158fn install_optional_extensions(paths: &PglitePaths) -> Result<()> {
159    let dir = assets_dir().join("extensions");
160    if !dir.exists() {
161        return Ok(());
162    }
163
164    for entry in fs::read_dir(&dir)? {
165        let path = entry?.path();
166        if !path
167            .file_name()
168            .and_then(|s| s.to_str())
169            .map(|name| name.ends_with(".tar.gz"))
170            .unwrap_or(false)
171        {
172            continue;
173        }
174
175        let ext_name = path
176            .file_name()
177            .and_then(|s| s.to_str())
178            .and_then(|name| name.strip_suffix(".tar.gz"))
179            .unwrap_or("");
180
181        let control_path = paths
182            .pgroot
183            .join("pglite/share/extension")
184            .join(format!("{}.control", ext_name));
185        if control_path.exists() {
186            continue;
187        }
188
189        install_extension_archive(paths, &path)?;
190    }
191
192    Ok(())
193}
194
195pub fn install_extension_archive(paths: &PglitePaths, archive_path: &Path) -> Result<()> {
196    let file = fs::File::open(archive_path)
197        .with_context(|| format!("open extension archive {}", archive_path.display()))?;
198    install_extension_reader(paths, file)
199}
200
201pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> {
202    install_extension_reader(paths, Cursor::new(bytes))
203}
204
205fn install_extension_reader<R: Read>(paths: &PglitePaths, reader: R) -> Result<()> {
206    let gz = GzDecoder::new(reader);
207    let mut ar = Archive::new(gz);
208    let target = paths.pgroot.join("pglite");
209    fs::create_dir_all(&target)?;
210    ar.unpack(&target)
211        .with_context(|| format!("unpack extension into {}", target.display()))?;
212    Ok(())
213}
214
215#[derive(Debug, Clone)]
216pub struct PglitePaths {
217    pub pgroot: PathBuf,
218    pub pgdata: PathBuf,
219}
220
221impl PglitePaths {
222    pub fn new(app_qual: (&str, &str, &str)) -> Result<Self> {
223        let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2)
224            .context("could not resolve app data dir")?;
225        let app_dir = pd.data_dir().to_path_buf();
226        let pgroot = app_dir.join("pglite");
227        let pgdata = app_dir.join("db");
228        Ok(Self { pgroot, pgdata })
229    }
230
231    pub fn with_root(root: impl Into<PathBuf>) -> Self {
232        let pgroot = root.into();
233        let pgdata = pgroot.join("pglite").join("base");
234        Self { pgroot, pgdata }
235    }
236
237    pub fn with_paths(pgroot: impl Into<PathBuf>, pgdata: impl Into<PathBuf>) -> Self {
238        Self {
239            pgroot: pgroot.into(),
240            pgdata: pgdata.into(),
241        }
242    }
243
244    /// Detect the legacy/local mount layouts that the Python helper supports.
245    pub fn detect_existing_mounts() -> Option<Self> {
246        for raw in ["tmp", "/tmp"] {
247            let base = PathBuf::from(raw);
248            let pgdata = base.join("pglite").join("base");
249            if pgdata.join("PG_VERSION").exists() {
250                return Some(Self {
251                    pgroot: base,
252                    pgdata,
253                });
254            }
255        }
256        None
257    }
258
259    pub fn mount_root(&self) -> &Path {
260        &self.pgroot
261    }
262
263    fn marker_runtime(&self) -> PathBuf {
264        self.pgroot.join(".runtime_ready")
265    }
266    fn marker_cluster(&self) -> PathBuf {
267        self.pgdata.join("PG_VERSION")
268    }
269}
270
271fn promote_nested_runtime(paths: &PglitePaths) -> Result<()> {
272    let nested = paths.pgroot.join("tmp").join("pglite");
273    let nested_bin = nested.join("bin");
274    if nested_bin.join("pglite.wasi").exists() {
275        for entry in std::fs::read_dir(&nested).context("read nested pglite dir")? {
276            let entry = entry?;
277            let name = entry.file_name();
278            let src = entry.path();
279            let dst = paths.pgroot.join(name);
280            let metadata = match std::fs::symlink_metadata(&dst) {
281                Ok(metadata) => Some(metadata),
282                Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
283                Err(err) => {
284                    return Err(err).with_context(|| format!("inspect {}", dst.display()));
285                }
286            };
287            if let Some(metadata) = metadata {
288                if metadata.file_type().is_dir() {
289                    std::fs::remove_dir_all(&dst)
290                        .with_context(|| format!("remove dir {}", dst.display()))?;
291                } else {
292                    std::fs::remove_file(&dst)
293                        .with_context(|| format!("remove file {}", dst.display()))?;
294                }
295            }
296            std::fs::rename(&src, &dst)
297                .with_context(|| format!("promote {} -> {}", src.display(), dst.display()))?;
298        }
299        let _ = std::fs::remove_dir_all(paths.pgroot.join("tmp"));
300    }
301    Ok(())
302}
303
304fn ensure_pglite_layout(paths: &PglitePaths) -> Result<()> {
305    let pglite_dir = paths.pgroot.join("pglite");
306    if !pglite_dir.exists() {
307        fs::create_dir_all(&pglite_dir)?;
308    }
309
310    for name in ["bin", "share", "lib", "password"] {
311        let src = paths.pgroot.join(name);
312        if src.exists() {
313            let dst = pglite_dir.join(name);
314            let moved = std::fs::rename(&src, &dst).is_ok();
315            if !moved {
316                if src.is_dir() {
317                    std::fs::create_dir_all(&dst)?;
318                    copy_dir_all(&src, &dst).with_context(|| {
319                        format!("copy dir {} -> {}", src.display(), dst.display())
320                    })?;
321                    std::fs::remove_dir_all(&src)?;
322                } else {
323                    std::fs::copy(&src, &dst).with_context(|| {
324                        format!("copy file {} -> {}", src.display(), dst.display())
325                    })?;
326                    std::fs::remove_file(&src)?;
327                }
328            }
329        }
330    }
331    Ok(())
332}
333
334pub(crate) fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> {
335    let pglite_dir = paths.pgroot.join("pglite");
336    if !pglite_dir.exists() {
337        return None;
338    }
339    let pglite_bin_dir = pglite_dir.join("bin");
340    let module = if pglite_bin_dir.join("pglite.wasi").exists() {
341        pglite_bin_dir.join("pglite.wasi")
342    } else {
343        return None;
344    };
345
346    let share = pglite_dir.join("share").join("postgresql");
347    if !share.exists() || !share.is_dir() {
348        return None;
349    }
350    if !share.join("postgres.bki").exists() {
351        return None;
352    }
353    Some((module, pglite_bin_dir))
354}
355
356fn finalize_runtime_setup(
357    paths: &PglitePaths,
358    module_path: &Path,
359    pglite_bin_dir: &Path,
360) -> Result<()> {
361    ensure_shim(module_path, &pglite_bin_dir.join("initdb"))?;
362    ensure_shim(module_path, &pglite_bin_dir.join("postgres"))?;
363    fs::write(paths.marker_runtime(), b"ok")?;
364    Ok(())
365}
366
367pub fn ensure_runtime(paths: &PglitePaths) -> Result<()> {
368    if let Some((module_path, bin_dir)) = locate_runtime_module(paths) {
369        install_optional_pg_dump(paths)?;
370        install_optional_extensions(paths)?;
371        finalize_runtime_setup(paths, &module_path, &bin_dir)?;
372        return Ok(());
373    }
374
375    if paths.marker_runtime().exists() {
376        let _ = fs::remove_file(paths.marker_runtime());
377    }
378
379    fs::create_dir_all(&paths.pgroot).context("create pgroot dir")?;
380    promote_nested_runtime(paths)?;
381    ensure_pglite_layout(paths)?;
382    install_optional_pg_dump(paths)?;
383    install_optional_extensions(paths)?;
384
385    if let Some((module_path, bin_dir)) = locate_runtime_module(paths) {
386        finalize_runtime_setup(paths, &module_path, &bin_dir)?;
387        return Ok(());
388    }
389
390    if let Ok(override_path) = std::env::var("PGLITE_OXIDE_TAR_XZ") {
391        let file = std::fs::File::open(&override_path)
392            .with_context(|| format!("open override tar.xz: {}", override_path))?;
393        let mut decoder = XzDecoder::new(file);
394        let mut ar = Archive::new(&mut decoder);
395        ar.unpack(&paths.pgroot)
396            .with_context(|| format!("unpack override tar.xz from {}", override_path))?;
397    } else {
398        let mut decoder = XzDecoder::new(EMBEDDED_TAR_XZ);
399        let mut ar = Archive::new(&mut decoder);
400        ar.unpack(&paths.pgroot)
401            .context("unpack embedded pglite-wasi.tar.xz")?;
402    }
403
404    promote_nested_runtime(paths)?;
405    ensure_pglite_layout(paths)?;
406    install_optional_pg_dump(paths)?;
407    install_optional_extensions(paths)?;
408
409    let (module_path, bin_dir) = locate_runtime_module(paths).ok_or_else(|| {
410        anyhow!(
411            "runtime missing: could not locate module under {} after install",
412            paths.pgroot.display()
413        )
414    })?;
415
416    finalize_runtime_setup(paths, &module_path, &bin_dir)
417}
418
419#[allow(clippy::const_is_empty)]
420pub fn embedded_runtime_present() -> bool {
421    !EMBEDDED_TAR_XZ.is_empty()
422}
423
424pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> {
425    if paths.marker_cluster().exists() {
426        return Ok(());
427    }
428
429    ensure_runtime(paths)?;
430    fs::create_dir_all(&paths.pgdata).context("create pgdata dir")?;
431
432    // Password file expected at /password (relative to PREFIX)
433    let pw_path = paths.pgroot.join("pglite").join("password");
434    if !pw_path.exists() {
435        fs::write(&pw_path, "localdevpassword\n").context("write password file")?;
436    }
437
438    let mut cfg = wasmtime::Config::new();
439    cfg.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
440    let engine = Engine::new(&cfg)?;
441
442    let pglite_bin_dir = paths.pgroot.join("pglite").join("bin");
443    let module_path = pglite_bin_dir.join("pglite.wasi");
444    let module = Module::from_file(&engine, &module_path)
445        .with_context(|| format!("load module at {}", module_path.display()))?;
446
447    let mut linker: Linker<WasiP1Ctx> = Linker::new(&engine);
448    add_to_linker_sync(&mut linker, |cx: &mut WasiP1Ctx| cx)?;
449
450    // Ensure cluster dir is empty for clean init
451    if paths.pgdata.exists() {
452        for entry in std::fs::read_dir(&paths.pgdata)? {
453            let entry = entry?;
454            if entry.file_name() != "PG_VERSION" {
455                let path = entry.path();
456                if path.is_dir() {
457                    std::fs::remove_dir_all(&path)?;
458                } else {
459                    std::fs::remove_file(&path)?;
460                }
461            }
462        }
463    }
464
465    // Build WASI ctx/instance
466    let mut b = standard_wasi_builder(paths)?;
467
468    // Boot argv mirrors: `/tmp/pglite/bin/postgres --single postgres`
469    let wasi = b
470        .args(&["/tmp/pglite/bin/postgres", "--single", "postgres"])
471        .build();
472
473    let mut store = Store::new(&engine, WasiP1Ctx::new(wasi));
474    let instance = linker.instantiate(&mut store, &module)?;
475
476    // 1) Embed setup first
477    info!("[pglite_oxide] Starting embed setup...");
478    if let Ok(start) = instance.get_typed_func::<(), ()>(&mut store, "_start") {
479        let _ = start.call(&mut store, ());
480        info!("[pglite_oxide] Embed setup completed");
481    } else {
482        warn!("[pglite_oxide] No _start export found");
483    }
484
485    // 2) Run initdb on the SAME instance; it reads env (PGDATA, PGSHAREDIR, POSTGRES, PREFIX)
486    debug!("[pglite_oxide] Looking for initdb export...");
487    let initdb = instance.get_typed_func::<(), i32>(&mut store, "pgl_initdb")?;
488
489    info!("[pglite_oxide] Calling initdb...");
490    let rc = initdb.call(&mut store, ())?;
491    info!("[pglite_oxide] initdb returned: {}", rc);
492    // Some pglite builds return non-zero even on success; trust the marker.
493    if !paths.marker_cluster().exists() {
494        anyhow::bail!("pgl_initdb rc={rc} but PG_VERSION not created");
495    }
496
497    // Best-effort graceful shutdown if exposed
498    if let Ok(shutdown) = instance.get_typed_func::<(), ()>(&mut store, "pgl_shutdown") {
499        let _ = shutdown.call(&mut store, ());
500    }
501
502    Ok(())
503}
504
505#[derive(Debug, Clone, Copy)]
506pub struct InstallOptions {
507    pub ensure_cluster: bool,
508}
509
510impl Default for InstallOptions {
511    fn default() -> Self {
512        Self {
513            ensure_cluster: true,
514        }
515    }
516}
517
518pub fn install_and_init(app_qual: (&str, &str, &str)) -> Result<PglitePaths> {
519    if let Some(existing) = PglitePaths::detect_existing_mounts() {
520        info!(
521            "[pglite_oxide] Reusing existing runtime at {}",
522            existing.pgroot.display()
523        );
524        return install_with_options(existing, InstallOptions::default());
525    }
526
527    let paths = PglitePaths::new(app_qual)?;
528    install_with_options(paths, InstallOptions::default())
529}
530
531pub fn install_and_init_with_paths(paths: PglitePaths) -> Result<PglitePaths> {
532    install_with_options(paths, InstallOptions::default())
533}
534
535pub fn install_and_init_in(root: impl Into<PathBuf>) -> Result<PglitePaths> {
536    let paths = PglitePaths::with_root(root);
537    install_with_options(paths, InstallOptions::default())
538}
539
540pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result<PglitePaths> {
541    ensure_runtime(&paths)?;
542    if options.ensure_cluster {
543        ensure_cluster(&paths)?;
544    }
545    Ok(paths)
546}
547
548#[derive(Debug, Clone)]
549pub struct MountInfo {
550    mount: PathBuf,
551    io_socket: PathBuf,
552    paths: PglitePaths,
553    reused_existing: bool,
554}
555
556impl MountInfo {
557    pub fn into_paths(self) -> PglitePaths {
558        self.paths
559    }
560
561    pub fn mount(&self) -> &Path {
562        &self.mount
563    }
564
565    pub fn io_socket(&self) -> &Path {
566        &self.io_socket
567    }
568
569    pub fn paths(&self) -> &PglitePaths {
570        &self.paths
571    }
572
573    pub fn reused_existing(&self) -> bool {
574        self.reused_existing
575    }
576}
577
578pub fn prepare_default_mount() -> Result<MountInfo> {
579    if let Some(existing) = PglitePaths::detect_existing_mounts() {
580        let reused_existing = true;
581        ensure_runtime(&existing)?;
582        if !existing.marker_cluster().exists() {
583            ensure_cluster(&existing)?;
584        }
585        let io_socket = resolve_io_socket(&existing);
586        return Ok(MountInfo {
587            mount: existing.pgroot.clone(),
588            io_socket,
589            paths: existing,
590            reused_existing,
591        });
592    }
593
594    let local_paths = PglitePaths::with_root(PathBuf::from("tmp"));
595    install_with_options(
596        local_paths.clone(),
597        InstallOptions {
598            ensure_cluster: false,
599        },
600    )?;
601    if !local_paths.marker_cluster().exists() {
602        ensure_cluster(&local_paths)?;
603    }
604    let io_socket = resolve_io_socket(&local_paths);
605    Ok(MountInfo {
606        mount: local_paths.pgroot.clone(),
607        io_socket,
608        paths: local_paths,
609        reused_existing: false,
610    })
611}
612
613fn resolve_io_socket(paths: &PglitePaths) -> PathBuf {
614    let mount = &paths.pgroot;
615    let io = paths.pgdata.join(".s.PGSQL.5432");
616    if mount.is_absolute() {
617        io
618    } else {
619        Path::new(".").join(io)
620    }
621}