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
22pub 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)?; fs::write(&urandom_path, buf)?; }
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 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 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 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 let mut b = standard_wasi_builder(paths)?;
467
468 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 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 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 if !paths.marker_cluster().exists() {
494 anyhow::bail!("pgl_initdb rc={rc} but PG_VERSION not created");
495 }
496
497 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}